diff --git a/CHANGELOG.md b/CHANGELOG.md index 2281156..98de6f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,19 @@ -## 0.0.1 +## 0.0.5 (https://github.com/ThomasDevApps/flutter_supabase_macro/pull/5) -Initial release : -- Creation of a `toJsonSupabase` which exclude the `primaryKey` from the `Map` +Add a named parameter for each field of the class. +For example, if class contain a field named `id` then `bool? removeId` +will be add as a named parameter for `toJsonSupabase`. + +If `removeId` is not null and true then, `id` will not be add in the json. -## 0.0.4 +## 0.0.4 (https://github.com/ThomasDevApps/flutter_supabase_macro/pull/4) Only exclude `primaryKey` from the Map if : - Can't be nullable then check that `!= null` -- The type is `String`, then check that the value `isNotEmpty` \ No newline at end of file +- The type is `String`, then check that the value `isNotEmpty` + + +## 0.0.1 (https://github.com/ThomasDevApps/flutter_supabase_macro/pull/1) + +Initial release : +- Creation of a `toJsonSupabase` which exclude the `primaryKey` from the `Map` \ No newline at end of file diff --git a/README.md b/README.md index a443d3b..953e0f0 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,13 @@ and the Flutter guide for --> # Flutter Supabase Macro +![testing workflow](https://github.com/ThomasDevApps/flutter_supabase_macro/actions/workflows/main.yml/badge.svg) + Package greatly inspired by `JsonCodable` (from Dart), makes it easy to create a JSON format of a template for Supabase. -| Before | After | -|----------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------| +| Before | After | +|----------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------| | ![before](https://raw.githubusercontent.com/ThomasDevApps/flutter_supabase_macro/main/assets/before.png) | ![after](https://raw.githubusercontent.com/ThomasDevApps/flutter_supabase_macro/main/assets/after.png) | - [What is a macro](#-what-is-a-macro) diff --git a/assets/before.png b/assets/before.png index 4f578b3..e5ebbd8 100644 Binary files a/assets/before.png and b/assets/before.png differ diff --git a/lib/flutter_supabase_macro.dart b/lib/flutter_supabase_macro.dart index fa2f131..a096856 100644 --- a/lib/flutter_supabase_macro.dart +++ b/lib/flutter_supabase_macro.dart @@ -7,6 +7,7 @@ import 'package:macros/macros.dart'; part 'src/extensions/code_extension.dart'; part 'src/extensions/iterable_extension.dart'; part 'src/extensions/named_type_annotation_extension.dart'; +part 'src/extensions/string_extension.dart'; part 'src/extensions/type_declaration_extension.dart'; part 'src/mixins/shared.dart'; part 'src/mixins/to_json_supabase.dart'; @@ -21,6 +22,7 @@ macro class FlutterSupabaseMacro implements ClassDeclarationsMacro, ClassDefinitionMacro { /// Primary key to exclude from the `toJsonSupabase`. + @override final String primaryKey; const FlutterSupabaseMacro({this.primaryKey = 'id'}); @@ -43,14 +45,11 @@ macro class FlutterSupabaseMacro ClassDeclaration clazz, TypeDefinitionBuilder builder, ) async { - final introspectionData = - await _SharedIntrospectionData.build(builder, clazz); - await _buildToJsonSupabase( - clazz, + final introspectionData = await _SharedIntrospectionData.build( builder, - introspectionData, - primaryKey, + clazz, ); + await _buildToJsonSupabase(clazz, builder, introspectionData); } } diff --git a/lib/src/extensions/string_extension.dart b/lib/src/extensions/string_extension.dart new file mode 100644 index 0000000..f33fe9f --- /dev/null +++ b/lib/src/extensions/string_extension.dart @@ -0,0 +1,10 @@ +part of '../../flutter_supabase_macro.dart'; + +extension _StringExtension on String { + /// Set the first character to upper case. + /// + /// ```dart + /// 'test of the function'.firstLetterUpperCase(); // 'Test of the function' + /// ``` + String _firstLetterToUpperCase() => "${this[0].toUpperCase()}${substring(1)}"; +} diff --git a/lib/src/mixins/to_json_supabase.dart b/lib/src/mixins/to_json_supabase.dart index b9bddf2..66247c9 100644 --- a/lib/src/mixins/to_json_supabase.dart +++ b/lib/src/mixins/to_json_supabase.dart @@ -1,6 +1,10 @@ +// ignore_for_file: deprecated_member_use + part of '../../flutter_supabase_macro.dart'; mixin _ToJsonSupabase on _Shared { + String get primaryKey; + /// Declare the [_toJsonMethodName] method. Future _declareToJsonSupabase( ClassDeclaration clazz, @@ -10,13 +14,47 @@ mixin _ToJsonSupabase on _Shared { // Check that no toJsonSupabase method exist final checkNoToJson = await _checkNoToJson(builder, clazz); if (!checkNoToJson) return; + final boolId = await builder.resolveIdentifier(_dartCore, 'bool'); + final boolCode = NamedTypeAnnotationCode(name: boolId); + final fields = await builder.fieldsOf(clazz); builder.declareInType( - DeclarationCode.fromParts( - [' external ', mapStringObject, ' $_toJsonMethodName();\n'], - ), + DeclarationCode.fromParts([ + ' external ', + mapStringObject, + ' $_toJsonMethodName(', + if (fields.isNotEmpty) '{\n', + if (fields.isNotEmpty) ..._createNamedParams(boolCode, fields), + if (fields.isNotEmpty) '\n }', + ');\n' + ]), ); } + /// Create `List` of parts. + /// + /// Example : [fields] contain one element named `firstField`, it will add : + /// ```dart + /// ' bool? removeFirstField,' + /// ``` + List _createNamedParams( + NamedTypeAnnotationCode boolCode, + List fields, + ) { + final list = []; + for (final field in fields) { + list.addAll([ + ' ', + boolCode, + '? ', + 'remove', + field.identifier.name._firstLetterToUpperCase(), + ',', + if (field != fields.last) '\n', + ]); + } + return list; + } + /// Emits an error [Diagnostic] if there is an existing [_toJsonMethodName] /// method on [clazz]. /// @@ -51,7 +89,6 @@ mixin _ToJsonSupabase on _Shared { ClassDeclaration clazz, TypeDefinitionBuilder typeBuilder, _SharedIntrospectionData introspectionData, - String primaryKey, ) async { // Get all methods of the class final methods = await typeBuilder.methodsOf(clazz); @@ -84,7 +121,6 @@ mixin _ToJsonSupabase on _Shared { field, builder, introspectionData, - isPrimaryKey: field.identifier.name == primaryKey, ), ), ), @@ -93,15 +129,33 @@ mixin _ToJsonSupabase on _Shared { parts.add('return json;\n }'); builder.augment( FunctionBodyCode.fromParts(parts), - docComments: CommentCode.fromParts([ - ' /// Map representing the model in json format for Supabase.\n', - ' ///\n', - ' /// The primary key [${fields.first.identifier.name}]', - ' is exclude from the map if empty.' - ]), + docComments: _createDocumentationForMethod(fields), ); } + /// Create the documentation for [_toJsonMethodName] method + /// according with [fields]. + CommentCode _createDocumentationForMethod(List fields) { + return CommentCode.fromParts([ + ' /// Map representing the model in json format for Supabase.\n', + ' ///\n', + ' /// The primary key [${fields.first.identifier.name}]', + ' is exclude from the map if empty.\n', + ' ///\n', + ' /// ', + ...fields.map((f) { + return [ + '[remove', + f.identifier.name._firstLetterToUpperCase(), + ']', + if (f != fields.last) ', ' + ].join(); + }), + ' can be set for remove field\n' + ' /// from the json.' + ]); + } + /// Returns void if [toJsonSupabase] not exist. /// /// Otherwise it will check that [toJsonSupabase] is valid with [_checkValidToJson]. @@ -131,9 +185,7 @@ mixin _ToJsonSupabase on _Shared { final methodIsMap = await methodReturnType.isExactly( introspectionData.jsonMapType, ); - if (method.namedParameters.isNotEmpty || - method.positionalParameters.isNotEmpty || - !methodIsMap) { + if (!methodIsMap) { builder.report( Diagnostic( DiagnosticMessage( @@ -241,13 +293,17 @@ mixin _ToJsonSupabase on _Shared { Future addEntryForField( FieldDeclaration field, DefinitionBuilder builder, - _SharedIntrospectionData introspectionData, { - bool isPrimaryKey = false, - }) async { + _SharedIntrospectionData introspectionData, + ) async { final parts = []; + final isPrimaryKey = field.identifier.name == primaryKey; final doNullCheck = field.type.isNullable; final needCondition = doNullCheck || isPrimaryKey; + final fieldName = field.identifier.name._firstLetterToUpperCase(); // Begin the definition of the condition + parts.addAll([ + 'if (remove$fieldName==null || !remove$fieldName) {\n ', + ]); if (needCondition) { parts.addAll(['if (']); } @@ -265,7 +321,7 @@ mixin _ToJsonSupabase on _Shared { } } // Close definition of the condition and open it - if (needCondition) parts.add(') {\n '); + if (needCondition) parts.add(') {\n '); // Add the field in the json parts.addAll([ "json[r'", @@ -284,8 +340,9 @@ mixin _ToJsonSupabase on _Shared { ]); // Close the condition if (needCondition) { - parts.add('}\n '); + parts.add(' }\n '); } + parts.add('}\n '); return RawCode.fromParts(parts); } diff --git a/lib/src/models/shared_introspection_data.dart b/lib/src/models/shared_introspection_data.dart index 817059c..01c2e0e 100644 --- a/lib/src/models/shared_introspection_data.dart +++ b/lib/src/models/shared_introspection_data.dart @@ -26,6 +26,9 @@ final class _SharedIntrospectionData { /// A [Code] representation of the type `dynamic`. final NamedTypeAnnotationCode dynamicCode; + /// A [Code] representation of the type [bool]. + final NamedTypeAnnotationCode boolCode; + /// A [Code] representation of the type [String]. final NamedTypeAnnotationCode stringCode; @@ -41,18 +44,20 @@ final class _SharedIntrospectionData { required this.mapEntry, required this.dynamicCode, required this.stringCode, + required this.boolCode, required this.superclass, }); static Future<_SharedIntrospectionData> build( DeclarationPhaseIntrospector builder, ClassDeclaration clazz) async { // Resolve identifiers - final (list, map, mapEntry, dynamic, string) = await ( + final (list, map, mapEntry, dynamic, string, bool) = await ( builder.resolveIdentifier(_dartCore, 'List'), builder.resolveIdentifier(_dartCore, 'Map'), builder.resolveIdentifier(_dartCore, 'MapEntry'), builder.resolveIdentifier(_dartCore, 'dynamic'), builder.resolveIdentifier(_dartCore, 'String'), + builder.resolveIdentifier(_dartCore, 'bool'), ).wait; // Get all NamedTypeAnnotationCode @@ -65,6 +70,7 @@ final class _SharedIntrospectionData { dynamicCode, ]); final stringCode = NamedTypeAnnotationCode(name: string); + final boolCode = NamedTypeAnnotationCode(name: bool); // Get the class's superclass (if exist) final superclass = clazz.superclass; @@ -88,6 +94,7 @@ final class _SharedIntrospectionData { mapEntry: mapEntry, dynamicCode: dynamicCode, stringCode: stringCode, + boolCode: boolCode, superclass: superclassDecl as ClassDeclaration?, ); } diff --git a/pubspec.yaml b/pubspec.yaml index 6e19d0e..417fe40 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_supabase_macro description: "A new Flutter project." -version: 0.0.4 +version: 0.0.5 homepage: environment: @@ -17,39 +17,5 @@ dev_dependencies: sdk: flutter flutter_lints: ^5.0.0 -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. flutter: - # To add assets to your package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/to/asset-from-package - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/to/resolution-aware-images - - # To add custom fonts to your package, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts in packages, see - # https://flutter.dev/to/font-from-package diff --git a/test/flutter_supabase_macro_test.dart b/test/flutter_supabase_macro_test.dart index b558d7e..517efce 100644 --- a/test/flutter_supabase_macro_test.dart +++ b/test/flutter_supabase_macro_test.dart @@ -11,22 +11,52 @@ class User { } void main() { - test('Test that id is missing from the json because is empty', () { - final user = User(id: '', name: 'Toto', age: 22); - final json = user.toJsonSupabase(); + group('Test the removal of the primaryKey `id`', () { + test('Test that `id` is remove from the json because is empty', () { + final user = User(id: '', name: 'Toto', age: 22); + final json = user.toJsonSupabase(); - expect(json.keys.length, 2); - expect(json['name'], 'Toto'); - expect(json['age'], 22); + expect(json.keys.length, 2); + expect(json['name'], 'Toto'); + expect(json['age'], 22); + }); + + test('Test that `id` is NOT remove from the json because is NOT empty', () { + final user = User(id: 'id-123', name: 'Toto', age: 22); + final json = user.toJsonSupabase(); + + expect(json.keys.length, 3); + expect(json['id'], 'id-123'); + expect(json['name'], 'Toto'); + expect(json['age'], 22); + }); }); - test('Test that id is NOT missing from the json because is NOT empty', () { - final user = User(id: 'id-123', name: 'Toto', age: 22); - final json = user.toJsonSupabase(); + group('Test hidings', () { + final user = User(id: '1234', name: 'Francisa', age: 45); + + test('Test `id` is remove from the json', () { + final json = user.toJsonSupabase(removeId: true); + + expect(json.keys.length, 2); + expect(json['name'], 'Francisa'); + expect(json['age'], 45); + }); + + test('Test `name` is remove from the json', () { + final json = user.toJsonSupabase(removeName: true); + + expect(json.keys.length, 2); + expect(json['id'], '1234'); + expect(json['age'], 45); + }); + + test('Test `age` is remove from the json', () { + final json = user.toJsonSupabase(removeAge: true); - expect(json.keys.length, 3); - expect(json['id'], 'id-123'); - expect(json['name'], 'Toto'); - expect(json['age'], 22); + expect(json.keys.length, 2); + expect(json['id'], '1234'); + expect(json['name'], 'Francisa'); + }); }); }