diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..352cb64 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,31 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Main CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + test-flutter: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2.16.0 + with: + channel: 'beta' + + - name: Install dependencies + run: flutter pub get + + - name: Analyze project source + run: flutter analyze + + - name: Run tests + run: flutter test --enable-experiment=macros \ No newline at end of file diff --git a/README.md b/README.md index 4a260d8..72b92bc 100644 --- a/README.md +++ b/README.md @@ -10,30 +10,56 @@ For general information about developing packages, see the Dart guide for and the Flutter guide for [developing packages and plugins](https://flutter.dev/to/develop-packages). --> +# Flutter Supabase Macro -TODO: Put a short description of the package here that helps potential users -know whether this package might be useful for them. +Package greatly inspired by `JsonCodable` (from Dart), makes it easy to create a JSON format of a template for Supabase. -## Features +## 🚀 Getting started -TODO: List what your package can do. Maybe include images, gifs, or videos. +Because the macros are still under development, you need to follow these instructions to be able to test this package : https://dart.dev/language/macros#set-up-the-experiment -## Getting started +Then add in your `pubspec.yaml` : -TODO: List prerequisites and provide or point to information on how to -start using the package. +```yaml +flutter_supabase_macro: + git: + url: https://github.com/ThomasDevApps/flutter_supabase_macro.git +``` + +## 🔎 How it works +Let's imagine the `User` class : -## Usage +```dart +class User { + final String id; + final String name; + final int age; + + const User({required this.id, required this.name, required this.age}); +} +``` +Let's assume that in your Supabase `users` table, the primary key is named `id`. -TODO: Include short and useful examples for package users. Add longer examples -to `/example` folder. +All you need to do is add the following : ```dart -const like = 'sample'; +@FlutterSupabaseMacro(primaryKey: 'id') // Add this (primaryKey is 'id' by default) +class User { + // ... +} ``` +It will generate a `toJsonSupabase()` method that returns a +`Map` that does not contain the `primaryKey` +(`id` in this case) : + +```dart +final user = User(id: 'id', name: 'Toto', age: 22); +final json = user.toJsonSupabase(); +print(json); // {} +``` + +## 📖 Additional information -## Additional information +This package is still undergoing experimentation, and is in no way intended for use in production apps. -TODO: Tell users more about the package: where to find more information, how to -contribute to the package, how to file issues, what response they can expect -from the package authors, and more. +Not officially affiliated with Supabase. diff --git a/analysis_options.yaml b/analysis_options.yaml index b6ea138..e096fb6 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -4,5 +4,7 @@ include: package:flutter_lints/flutter.yaml # https://dart.dev/guides/language/analysis-options analyzer: + errors: + non_part_of_directive_in_part: ignore enable-experiment: - macros \ No newline at end of file diff --git a/lib/flutter_supabase_macro.dart b/lib/flutter_supabase_macro.dart index 5933909..29f620e 100644 --- a/lib/flutter_supabase_macro.dart +++ b/lib/flutter_supabase_macro.dart @@ -1,3 +1,3 @@ library; -export 'src/annotations.dart'; +export 'src/supabase_macro.dart'; diff --git a/lib/src/annotations.dart b/lib/src/annotations.dart deleted file mode 100644 index 80e5475..0000000 --- a/lib/src/annotations.dart +++ /dev/null @@ -1,114 +0,0 @@ -library; - -import 'dart:async'; - -import 'package:collection/collection.dart'; -import 'package:macros/macros.dart'; - -macro class FlutterSupabaseMacro implements ClassDeclarationsMacro, ClassDefinitionMacro { - const FlutterSupabaseMacro(); - - @override - FutureOr buildDeclarationsForClass(ClassDeclaration clazz, MemberDeclarationBuilder builder,) async { - await Future.wait([ - const AutoConstructor().buildDeclarationsForClass(clazz, builder) - ]); - } - - @override - FutureOr buildDefinitionForClass(ClassDeclaration clazz, TypeDefinitionBuilder builder) { - - } -} - -macro class AutoConstructor implements ClassDeclarationsMacro { - const AutoConstructor(); - - @override - Future buildDeclarationsForClass( - ClassDeclaration clazz, MemberDeclarationBuilder builder) async { - var constructors = await builder.constructorsOf(clazz); - if (constructors.any((c) => c.identifier.name == '')) { - throw ArgumentError( - 'Cannot generate an unnamed constructor because one already exists'); - } - - var params = []; - // Add all the fields of `declaration` as named parameters. - var fields = await builder.fieldsOf(clazz); - if (fields.isNotEmpty) { - for (var field in fields) { - var requiredKeyword = field.type.isNullable ? '' : 'required '; - params.addAll(['\n${requiredKeyword}', field.identifier, ',']); - } - } - - // The object type from dart:core. - var objectType = await builder.resolve(NamedTypeAnnotationCode( - name: - // ignore: deprecated_member_use - await builder.resolveIdentifier(Uri.parse('dart:core'), 'Object'))); - - // Add all super constructor parameters as named parameters. - var superclass = clazz.superclass == null - ? null - : await builder.typeDeclarationOf(clazz.superclass!.identifier); - var superType = superclass == null - ? null - : await builder - .resolve(NamedTypeAnnotationCode(name: superclass.identifier)); - MethodDeclaration? superconstructor; - if (superType != null && (await superType.isExactly(objectType)) == false) { - superconstructor = (await builder.constructorsOf(superclass!)) - .firstWhereOrNull((c) => c.identifier.name == ''); - if (superconstructor == null) { - throw ArgumentError( - 'Super class $superclass of $clazz does not have an unnamed ' - 'constructor'); - } - // We convert positional parameters in the super constructor to named - // parameters in this constructor. - for (var param in superconstructor.positionalParameters) { - var requiredKeyword = param.isRequired ? 'required' : ''; - params.addAll([ - '\n$requiredKeyword', - param.type.code, - ' ${param.identifier.name},', - ]); - } - for (var param in superconstructor.namedParameters) { - var requiredKeyword = param.isRequired ? '' : 'required '; - params.addAll([ - '\n$requiredKeyword', - param.type.code, - ' ${param.identifier.name},', - ]); - } - } - - bool hasParams = params.isNotEmpty; - List parts = [ - // Don't use the identifier here because it should just be the raw name. - clazz.identifier.name, - '(', - if (hasParams) '{', - ...params, - if (hasParams) '}', - ')', - ]; - if (superconstructor != null) { - parts.addAll([' : super(']); - for (var param in superconstructor.positionalParameters) { - parts.add('\n${param.identifier.name},'); - } - if (superconstructor.namedParameters.isNotEmpty) { - for (var param in superconstructor.namedParameters) { - parts.add('\n${param.identifier.name}: ${param.identifier.name},'); - } - } - parts.add(')'); - } - parts.add(';'); - builder.declareInType(DeclarationCode.fromParts(parts)); - } -} \ No newline at end of file diff --git a/lib/src/mixins/shared.dart b/lib/src/mixins/shared.dart new file mode 100644 index 0000000..80cd486 --- /dev/null +++ b/lib/src/mixins/shared.dart @@ -0,0 +1,71 @@ +// ignore_for_file: deprecated_member_use, unintended_html_in_doc_comment + +part of '../supabase_macro.dart'; + +/// Shared logic for all macros which run in the declarations phase. +mixin _Shared { + /// Returns [type] as a [NamedTypeAnnotation] if it is one, otherwise returns + /// `null` and emits relevant error diagnostics. + NamedTypeAnnotation? _checkNamedType(TypeAnnotation type, Builder builder) { + if (type is NamedTypeAnnotation) return type; + if (type is OmittedTypeAnnotation) { + builder.report( + _createDiagnostic( + type, + message: + 'Only fields with explicit types are allowed on serializable ' + 'classes, please add a type.', + ), + ); + } else { + builder.report( + _createDiagnostic( + type, + message: 'Only fields with named types are allowed on serializable ' + 'classes.', + ), + ); + } + return null; + } + + /// Create a [Diagnostic] according with [type] and [message]. + Diagnostic _createDiagnostic(TypeAnnotation type, {required String message}) { + return Diagnostic( + DiagnosticMessage(message, target: type.asDiagnosticTarget), + Severity.error, + ); + } + + /// Does some basic validation on [clazz], and shared setup logic. + /// + /// Returns a code representation of the [Map] class. + Future _setup( + ClassDeclaration clazz, + MemberDeclarationBuilder builder, + ) async { + if (clazz.typeParameters.isNotEmpty) { + throw DiagnosticException( + Diagnostic( + DiagnosticMessage( + 'Cannot be applied to classes with generic type parameters', + ), + Severity.error, + ), + ); + } + + final (map, string, dynamic) = await ( + builder.resolveIdentifier(_dartCore, 'Map'), + builder.resolveIdentifier(_dartCore, 'String'), + builder.resolveIdentifier(_dartCore, 'dynamic'), + ).wait; + return NamedTypeAnnotationCode( + name: map, + typeArguments: [ + NamedTypeAnnotationCode(name: string), + NamedTypeAnnotationCode(name: dynamic), + ], + ); + } +} diff --git a/lib/src/mixins/to_json_supabase.dart b/lib/src/mixins/to_json_supabase.dart new file mode 100644 index 0000000..161980d --- /dev/null +++ b/lib/src/mixins/to_json_supabase.dart @@ -0,0 +1,351 @@ +part of '../supabase_macro.dart'; + +mixin _ToJsonSupabase on _Shared { + /// Declare the [_toJsonMethodName] method. + Future _declareToJsonSupabase( + ClassDeclaration clazz, + MemberDeclarationBuilder builder, + NamedTypeAnnotationCode mapStringObject, + ) async { + // Check that no toJsonSupabase method exist + final checkNoToJson = await _checkNoToJson(builder, clazz); + if (!checkNoToJson) return; + builder.declareInType( + DeclarationCode.fromParts( + [' external ', mapStringObject, ' $_toJsonMethodName();'], + ), + ); + } + + /// Emits an error [Diagnostic] if there is an existing [_toJsonMethodName] + /// method on [clazz]. + /// + /// Returns `true` if the check succeeded (there was no `toJson`) and false + /// if it didn't (a diagnostic was emitted). + Future _checkNoToJson( + DeclarationBuilder builder, + ClassDeclaration clazz, + ) async { + final methods = await builder.methodsOf(clazz); + final toJsonSupabase = + methods.firstWhereOrNull((m) => m.identifier.name == _toJsonMethodName); + if (toJsonSupabase != null) { + builder.report( + Diagnostic( + DiagnosticMessage( + 'Cannot generate a toJson method due to this existing one.', + target: toJsonSupabase.asDiagnosticTarget, + ), + Severity.error, + ), + ); + return false; + } + return true; + } + + Future _buildToJsonSupabase( + ClassDeclaration clazz, + TypeDefinitionBuilder typeBuilder, + _SharedIntrospectionData introspectionData, + String primaryKey, + ) async { + // Get all methods of the class + final methods = await typeBuilder.methodsOf(clazz); + // Get the toJsonSupabase method (if exist) + final toJsonSupabase = methods.firstWhereOrNull( + (m) => m.identifier.name == _toJsonMethodName, + ); + // Do a initial check + await _initialCheck(toJsonSupabase, typeBuilder, introspectionData); + + // Get the FunctionDefinitionBuilder + final builder = await typeBuilder.buildMethod(toJsonSupabase!.identifier); + + // Check that superclass has toJsonSupabase + final superclassHasToJson = + await _checkSuperclassHasToJson(introspectionData, typeBuilder); + if (superclassHasToJson == null) return; + + // Create different parts + final parts = _createParts(introspectionData, + superclassHasToJson: superclassHasToJson); + + // Get all fields + final fields = introspectionData.fields.where((f) { + bool canBeAdd = f.identifier.name != primaryKey; + return canBeAdd; + }); + parts.addAll( + await Future.wait( + fields.map( + (field) => addEntryForField( + field, + builder, + toJsonSupabase, + introspectionData, + ), + ), + ), + ); + + parts.add('return json;\n }'); + builder.augment(FunctionBodyCode.fromParts(parts)); + } + + /// Returns void if [toJsonSupabase] not exist. + /// + /// Otherwise it will check that [toJsonSupabase] is valid with [_checkValidToJson]. + /// If it's not the case it will returns void. + Future _initialCheck( + MethodDeclaration? toJsonSupabase, + TypeDefinitionBuilder typeBuilder, + _SharedIntrospectionData introspectionData, + ) async { + if (toJsonSupabase == null) return; + final methodIsValid = await _checkValidToJson( + toJsonSupabase, + introspectionData, + typeBuilder, + ); + if (!methodIsValid) return; + } + + /// Check that [method] is a valid `toJson` method, throws a + /// [DiagnosticException] if not. + Future _checkValidToJson( + MethodDeclaration method, + _SharedIntrospectionData introspectionData, + DefinitionBuilder builder, + ) async { + final methodReturnType = await builder.resolve(method.returnType.code); + final methodIsMap = await methodReturnType.isExactly( + introspectionData.jsonMapType, + ); + if (method.namedParameters.isNotEmpty || + method.positionalParameters.isNotEmpty || + !methodIsMap) { + builder.report( + Diagnostic( + DiagnosticMessage( + 'Expected no parameters, and a return type of ' + 'Map', + target: method.asDiagnosticTarget, + ), + Severity.error, + ), + ); + return false; + } + return true; + } + + /// Check if the superclass (if exist) has a [_toJsonMethodName]. + Future _checkSuperclassHasToJson( + _SharedIntrospectionData introspectionData, + DefinitionBuilder builder, + ) async { + bool superclassHasToJson = false; + final superclassDeclaration = introspectionData.superclass; + final superClassIsObject = + superclassDeclaration?.isExactly('Object', _dartCore); + if (superclassDeclaration != null && !superClassIsObject!) { + final superclassMethods = await builder.methodsOf(superclassDeclaration); + for (final superMethod in superclassMethods) { + if (superMethod.identifier.name == _toJsonMethodName) { + final jsonMethodIsValid = + await _checkValidToJson(superMethod, introspectionData, builder); + if (!jsonMethodIsValid) return null; + superclassHasToJson = true; + break; + } + } + // If the superclass has not a toJsonSupabase method + if (!superclassHasToJson) { + builder.report( + Diagnostic( + DiagnosticMessage( + 'Serialization of classes that extend other classes is only ' + 'supported if those classes have a valid ' + '`Map $_toJsonMethodName()` method.', + target: introspectionData.clazz.superclass?.asDiagnosticTarget, + ), + Severity.error, + ), + ); + return null; + } + } + return superclassHasToJson; + } + + // TODO à doc + List _createParts( + _SharedIntrospectionData introspectionData, { + required bool superclassHasToJson, + }) { + return [ + '{\n final json = ', + if (superclassHasToJson) + 'super.$_toJsonMethodName()' + else ...[ + '<', + introspectionData.stringCode, + ', ', + introspectionData.dynamicCode, + '>{}', + ], + ';\n ', + ]; + } + + Future addEntryForField( + FieldDeclaration field, + DefinitionBuilder builder, + MethodDeclaration toJson, + _SharedIntrospectionData introspectionData, + ) async { + final parts = []; + final doNullCheck = field.type.isNullable; + if (doNullCheck) { + parts.addAll([ + 'if (', + field.identifier, + ' != null) {\n ', + ]); + } + parts.addAll([ + "json[r'", + field.identifier.name, + "'] = ", + await _convertTypeToJson( + field.type, + RawCode.fromParts([ + field.identifier, + if (doNullCheck) '!', + ]), + builder, + introspectionData, + ), + ';\n ', + ]); + if (doNullCheck) { + parts.add('}\n '); + } + return RawCode.fromParts(parts); + } + + // TODO à commenter + Future _convertTypeToJson( + TypeAnnotation rawType, + Code valueReference, + DefinitionBuilder builder, + _SharedIntrospectionData introspectionData, + ) async { + // Get the type of rawType + final type = _checkNamedType(rawType, builder); + if (type == null) { + return RawCode.fromString( + "throw 'Unable to serialize type ${rawType.code.debugString}'", + ); + } + // Get the class declaration of the type + final classDeclaration = await type.classDeclaration(builder); + if (classDeclaration == null) { + return RawCode.fromString( + "throw 'Unable to serialize type ${type.code.debugString}';", + ); + } + // Handle if the type is nullable + final nullCheck = type.isNullable + ? RawCode.fromParts([ + valueReference, + ' == null ? null : ', + ]) + : null; + + // Convert the type to a serialized one + final typeSerialized = await _serializeType(type, classDeclaration, + nullCheck, valueReference, builder, introspectionData); + if (typeSerialized != null) return typeSerialized; + + // Return toJsonSupabase method if already exist + final toJsonMethod = await _getToJsonMethod( + classDeclaration, builder, nullCheck, valueReference); + if (toJsonMethod != null) return toJsonMethod; + + // Unsupported type, report an error and return valid code that throws. + builder.report( + Diagnostic( + DiagnosticMessage( + 'Unable to serialize type, it must be a native JSON type or a ' + 'type with a `Map toJson()` method.', + target: type.asDiagnosticTarget), + Severity.error, + ), + ); + return RawCode.fromString( + "throw 'Unable to serialize type ${type.code.debugString}';"); + } + + /// TODO à doc + Future _serializeType( + NamedTypeAnnotation type, + ClassDeclaration classDeclaration, + RawCode? nullCheck, + Code valueReference, + DefinitionBuilder builder, + _SharedIntrospectionData introspectionData, + ) async { + if (classDeclaration.library.uri == _dartCore) { + switch (classDeclaration.identifier.name) { + case 'List' || 'Set': + return RawCode.fromParts([ + if (nullCheck != null) nullCheck, + '[ for (final item in ', + valueReference, + ') ', + await _convertTypeToJson(type.typeArguments.single, + RawCode.fromString('item'), builder, introspectionData), + ']', + ]); + case 'Map': + return RawCode.fromParts([ + if (nullCheck != null) nullCheck, + '{ for (final ', + introspectionData.mapEntry, + '(:key, :value) in ', + valueReference, + '.entries) key:', + await _convertTypeToJson(type.typeArguments.last, + RawCode.fromString('value'), builder, introspectionData), + '}', + ]); + case 'int' || 'double' || 'num' || 'String' || 'bool': + return valueReference; + } + } + return null; + } + + // TODO à commenter + Future? _getToJsonMethod( + ClassDeclaration classDeclaration, + DefinitionBuilder builder, + RawCode? nullCheck, + Code valueReference, + ) async { + final methods = await builder.methodsOf(classDeclaration); + final toJson = methods + .firstWhereOrNull((m) => m.identifier.name == _toJsonMethodName) + ?.identifier; + if (toJson != null) { + return RawCode.fromParts([ + if (nullCheck != null) nullCheck, + valueReference, + '.$_toJsonMethodName()' + ]); + } + return null; + } +} diff --git a/lib/src/supabase_macro.dart b/lib/src/supabase_macro.dart new file mode 100644 index 0000000..3ec1e67 --- /dev/null +++ b/lib/src/supabase_macro.dart @@ -0,0 +1,213 @@ +// ignore_for_file: deprecated_member_use, unintended_html_in_doc_comment +library; + +import 'dart:async'; + +import 'package:macros/macros.dart'; + +part 'mixins/shared.dart'; +part 'mixins/to_json_supabase.dart'; + +final _dartCore = Uri.parse('dart:core'); + +final _toJsonMethodName = 'toJsonSupabase'; + +macro class FlutterSupabaseMacro + with _Shared, _ToJsonSupabase + implements ClassDeclarationsMacro, ClassDefinitionMacro { + + final String primaryKey; + const FlutterSupabaseMacro({this.primaryKey = 'id'}); + + /// Declares the `fromJson` constructor and `toJsonSupabase` method, but does not + /// implement them. + @override + Future buildDeclarationsForClass( + ClassDeclaration clazz, + MemberDeclarationBuilder builder, + ) async { + final mapStringObject = await _setup(clazz, builder); + await _declareToJsonSupabase(clazz, builder, mapStringObject); + } + + /// Provides the actual definitions of the `fromJson` constructor and `toJsonSupabase` + /// method, which were declared in the previous phase. + @override + Future buildDefinitionForClass( + ClassDeclaration clazz, + TypeDefinitionBuilder builder, + ) async { + final introspectionData = + await _SharedIntrospectionData.build(builder, clazz); + await _buildToJsonSupabase( + clazz, + builder, + introspectionData, + primaryKey, + ); + } +} + +//////////////////////////////////////////////////////////////////////////////// + +/// This data is collected asynchronously, so we only want to do it once and +/// share that work across multiple locations. +final class _SharedIntrospectionData { + /// The declaration of the class we are generating for. + final ClassDeclaration clazz; + + /// All the fields on the [clazz]. + final List fields; + + /// A [Code] representation of the type [List]. + final NamedTypeAnnotationCode jsonListCode; + + /// A [Code] representation of the type [Map]. + final NamedTypeAnnotationCode jsonMapCode; + + /// The resolved [StaticType] representing the [Map] type. + final StaticType jsonMapType; + + /// The resolved identifier for the [MapEntry] class. + final Identifier mapEntry; + + /// A [Code] representation of the type [Object]. + final NamedTypeAnnotationCode dynamicCode; + + /// A [Code] representation of the type [String]. + final NamedTypeAnnotationCode stringCode; + + /// The declaration of the superclass of [clazz], if it is not [Object]. + final ClassDeclaration? superclass; + + _SharedIntrospectionData({ + required this.clazz, + required this.fields, + required this.jsonListCode, + required this.jsonMapCode, + required this.jsonMapType, + required this.mapEntry, + required this.dynamicCode, + required this.stringCode, + required this.superclass, + }); + + static Future<_SharedIntrospectionData> build( + DeclarationPhaseIntrospector builder, ClassDeclaration clazz) async { + final (list, map, mapEntry, dynamic, string) = await ( + builder.resolveIdentifier(_dartCore, 'List'), + builder.resolveIdentifier(_dartCore, 'Map'), + builder.resolveIdentifier(_dartCore, 'MapEntry'), + builder.resolveIdentifier(_dartCore, 'dynamic'), + builder.resolveIdentifier(_dartCore, 'String'), + ).wait; + final dynamicCode = NamedTypeAnnotationCode(name: dynamic); + final jsonListCode = NamedTypeAnnotationCode(name: list, typeArguments: [ + dynamicCode, + ]); + final jsonMapCode = NamedTypeAnnotationCode(name: map, typeArguments: [ + NamedTypeAnnotationCode(name: string), + dynamicCode, + ]); + final stringCode = NamedTypeAnnotationCode(name: string); + final superclass = clazz.superclass; + final (fields, jsonMapType, superclassDecl) = await ( + builder.fieldsOf(clazz), + builder.resolve(jsonMapCode), + superclass == null + ? Future.value(null) + : builder.typeDeclarationOf(superclass.identifier), + ).wait; + + return _SharedIntrospectionData( + clazz: clazz, + fields: fields, + jsonListCode: jsonListCode, + jsonMapCode: jsonMapCode, + jsonMapType: jsonMapType, + mapEntry: mapEntry, + dynamicCode: dynamicCode, + stringCode: stringCode, + superclass: superclassDecl as ClassDeclaration?, + ); + } +} + +//////////////////////////////////////////////////////////////////////////////// + +extension _FirstWhereOrNull on Iterable { + T? firstWhereOrNull(bool Function(T) compare) { + for (final item in this) { + if (compare(item)) return item; + } + return null; + } +} + +//////////////////////////////////////////////////////////////////////////////// + +extension _IsExactly on TypeDeclaration { + /// Cheaper than checking types using a [StaticType]. + bool isExactly(String name, Uri library) => + identifier.name == name && this.library.uri == library; +} + +//////////////////////////////////////////////////////////////////////////////// + +extension on Code { + /// Used for error messages. + String get debugString { + final buffer = StringBuffer(); + _writeDebugString(buffer); + return buffer.toString(); + } + + void _writeDebugString(StringBuffer buffer) { + for (final part in parts) { + switch (part) { + case Code(): + part._writeDebugString(buffer); + case Identifier(): + buffer.write(part.name); + case OmittedTypeAnnotation(): + buffer.write(''); + default: + buffer.write(part); + } + } + } +} + +//////////////////////////////////////////////////////////////////////////////// + +extension on NamedTypeAnnotation { + /// Follows the declaration of this type through any type aliases, until it + /// reaches a [ClassDeclaration], or returns null if it does not bottom out on + /// a class. + Future classDeclaration(DefinitionBuilder builder) async { + var typeDecl = await builder.typeDeclarationOf(identifier); + while (typeDecl is TypeAliasDeclaration) { + final aliasedType = typeDecl.aliasedType; + if (aliasedType is! NamedTypeAnnotation) { + builder.report(Diagnostic( + DiagnosticMessage( + 'Only fields with named types are allowed on serializable ' + 'classes', + target: asDiagnosticTarget), + Severity.error)); + return null; + } + typeDecl = await builder.typeDeclarationOf(aliasedType.identifier); + } + if (typeDecl is! ClassDeclaration) { + builder.report(Diagnostic( + DiagnosticMessage( + 'Only classes are supported as field types for serializable ' + 'classes', + target: asDiagnosticTarget), + Severity.error)); + return null; + } + return typeDecl; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index ecf91db..8b7fec1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,10 +8,8 @@ environment: flutter: ">=1.17.0" dependencies: - collection: ^1.19.0 flutter: sdk: flutter - json: ^0.20.2 macros: ^0.1.2-main.4 dev_dependencies: diff --git a/test/flutter_supabase_macro_test.dart b/test/flutter_supabase_macro_test.dart index 43aa1c3..b00c3ec 100644 --- a/test/flutter_supabase_macro_test.dart +++ b/test/flutter_supabase_macro_test.dart @@ -1,13 +1,22 @@ import 'package:flutter_supabase_macro/flutter_supabase_macro.dart'; import 'package:flutter_test/flutter_test.dart'; -@FlutterSupabaseMacro() -class TestMacro { +@FlutterSupabaseMacro(primaryKey: 'id') +class User { final String id; + final String name; + final int age; + + const User({required this.id, required this.name, required this.age}); } void main() { - test('a', () { - final test = TestMacro(id: 'daa'); + test('Test that id is missing from the json', () { + final user = User(id: 'id', name: 'Toto', age: 22); + final json = user.toJsonSupabase(); + + expect(json.keys.length, 2); + expect(json['name'], 'Toto'); + expect(json['age'], 22); }); }