diff --git a/example/test/ex11_generic_subclass_manual_test.dart b/example/test/ex11_generic_subclass_manual_test.dart index 4fafdff..69b4c30 100644 --- a/example/test/ex11_generic_subclass_manual_test.dart +++ b/example/test/ex11_generic_subclass_manual_test.dart @@ -5,9 +5,10 @@ import 'package:test/test.dart'; abstract class A { T1 get x; + T2 get y; - A copyWith_A({ + A copyWith_A({ Opt? x, Opt? y, }); @@ -26,7 +27,7 @@ class B implements A { String toString() => "(B-x:$x|y:$y|z:$z)"; - B copyWith_A({ + A copyWith_A({ Opt? x, Opt? y, }) { @@ -34,7 +35,7 @@ class B implements A { x: x == null ? this.x as int : x.value as int, y: y == null ? this.y as String : y.value as String, z: (this as B).z, - ); + ) as A; } B copyWith_B({ diff --git a/example/test/ex29_copywith_subclasses_test.dart b/example/test/ex29_copywith_subclasses_test.dart index cdec31b..97fccc3 100644 --- a/example/test/ex29_copywith_subclasses_test.dart +++ b/example/test/ex29_copywith_subclasses_test.dart @@ -85,7 +85,7 @@ main() { test("db", () { D db = D(b: 5, a: "A"); - var db_copy = db.copyWith_B(a: () => "a", b: () => 6); + var db_copy = db.copyWith_B(a: () => "a", b: () => 6) as D; expect(db_copy.toString(), "(D-a:a|b:6)"); }); diff --git a/example/test/ex60_copywith_generic_test.dart b/example/test/ex60_copywith_generic_test.dart index da969c5..c74794f 100644 --- a/example/test/ex60_copywith_generic_test.dart +++ b/example/test/ex60_copywith_generic_test.dart @@ -7,15 +7,11 @@ part 'ex60_copywith_generic_test.morphy.dart'; main() { test("1", () { - // var a = A(x:1, y:2); - // - // var aAsInt = a.copyWith_A(x: () => 2); - // - // expect(aAsInt.runtimeType, A); + var a = A(x:1, y:2); - // var aAsDouble = a.copyWith_A(x: () => 2.1); - // - // expect(aAsDouble.runtimeType, 2.1); + var aAsInt = a.copyWith_A(x: () => 2); + + expect(aAsInt.runtimeType, A); }); } diff --git a/example/test/ex63_class_with_no_members_test.dart b/example/test/ex63_class_with_no_members_test.dart new file mode 100644 index 0000000..7b1aacd --- /dev/null +++ b/example/test/ex63_class_with_no_members_test.dart @@ -0,0 +1,30 @@ +import 'package:morphy_annotation/morphy_annotation.dart'; +import 'package:test/test.dart'; + +part 'ex63_class_with_no_members_test.morphy.dart'; + +@Morphy( + explicitSubTypes: [ + $AgreedEulaState, + ] +) +abstract class $EulaState +{ + // lack of members +} + +@Morphy() +abstract class $AgreedEulaState implements $EulaState +{ + bool get test; +} + +main() { + test("1", () { + var a = EulaState(); + var b = AgreedEulaState(test: true); + + expect(a == null, false); + expect(b.test, true); + }); +} diff --git a/morphy/CHANGELOG.md b/morphy/CHANGELOG.md index 6500d1e..43e2d0b 100644 --- a/morphy/CHANGELOG.md +++ b/morphy/CHANGELOG.md @@ -1,8 +1,23 @@ +## 2.0.0 +- BREAKING CHANGES! copyWith_A is now copyWithA and changeTo_A is now changeToA +- This is to make the naming more consistent with dart conventions +- merged the PRs that allow to set explicitToJson false +- Call the class fromJson if _className_ isn't specified +- Support property name shadowing global types such as Type, String, In… +- thanks to @miklcct for the PRs. + +## 1.3.0 +- bug fixes, subclass with no members now works, generic copy with bug now fixed where type is incorrect +- in order to fix the above problem the type returned from the copywith is now the type of the named copywith function, eg b.copyWith_A now returns an A type +- this is a BREAKING change. +- if you specify D newD = d.copyWith_A(); then newD will be of type D not A and you'll receive an error +- the thing is that if you know it is a d type you'd more likely do a d.copyWith_D() so this should not be a problem + ## 1.2.0 - New functionality - private getters are now allowed! ## 1.1.0 -- Breaking change! copywith / change to, the Opt class has been removed and now we favour the () => syntax for optional parameters +- Breaking change! copywith / change to, the Opt class has been removed and now we favour the () => syntax for optional parameters ## 1.0.8 - change_to added for a subclass to change the type back to a superclass @@ -33,4 +48,4 @@ - Updated documentation ## 1.0.0 -- First published version \ No newline at end of file +- First published version diff --git a/morphy/README.md b/morphy/README.md index 5e089d5..e2f7d4d 100644 --- a/morphy/README.md +++ b/morphy/README.md @@ -7,8 +7,8 @@ We need a way to simplify our Dart classes, support polymorphism and allow copyi ### Why Not Freezed or Built Value? Well the main reason; I actually wrote this before Freezed was released. -Freezed is really good, established and well used. -However if you want to use both inheritance and polymorphism use Morphy. +Freezed is really good, established and well used. +However if you want to use both inheritance and polymorphism use Morphy. ### Solution: use morphy @@ -24,7 +24,7 @@ To create a new class. ``` import 'package:morphy_annotation/morphy_annotation.dart'; part 'Pet.morphy.dart'; - + @morphy abstract class $Pet { String get type; @@ -77,13 +77,13 @@ var cat1 = Pet(type: "cat"); ### CopyWith -A simple copy with implementation comes with every class. -We pass a function to the copyWith_Pet method that returns a new value to set the property. +A simple copy with implementation comes with every class. +We pass a function to the copyWithPet method that returns a new value to set the property. You can pass any value you like, including null if the property is nullable. ``` var flossy = Pet(name: "Flossy", age: 5); - var plossy = flossy.copyWith_Pet(name: () => "Plossy"); + var plossy = flossy.copyWithPet(name: () => "Plossy"); expect(flossy.age, plossy.age); ``` @@ -107,9 +107,9 @@ All properties will be inherited by default. abstract class $Cat implements $Pet { int get whiskerLength; } - + var bagpussCat = Cat(whiskerLength: 13.75, name: "Bagpuss", age: 4); - + expect(bagpussCat.whiskerLength, 13.75); ### CopyWith Polymorphic @@ -122,14 +122,14 @@ then the copyWith still works whilst preserving the underlying type. Cat(whiskerLength: 13.75, name: "Bagpuss", age: 4), Dog(woofSound: "rowf", name: "Colin", age: 4), ]; - + var petsOlder = pets // - .map((e) => e.copyWith_Pet(age: () => e.age + 1)) + .map((e) => e.copyWithPet(age: () => e.age + 1)) .toList(); - + expect(petsOlder[0].age, 5); expect(petsOlder[1].age, 5); - + Importantly the type remains the same, it is not converted to a Pet class. expect(petsOlder[0].runtimeType, Cat); @@ -147,7 +147,7 @@ Importantly the type remains the same, it is not converted to a Pet class. ### Generics are allowed -Specify the class definition if you want to constrain your generic type (use the dollar) +Specify the class definition if you want to constrain your generic type (use the dollar) @morphy abstract class $PetOwner { @@ -157,7 +157,7 @@ Specify the class definition if you want to constrain your generic type (use the var cathy = PetOwner(ownerName: "Cathy", pet: bagpussCat); var dougie = PetOwner(ownerName: "Dougie", pet: colin); - + expect(cathy.pet.whiskerLength, 13.75); expect(dougie.pet.woofSound, "rowf"); @@ -167,7 +167,7 @@ Sometimes you might want to turn a super class into a subclass (Pet into a Cat) var flossy = Pet(name: "Flossy", age: 5); - var bagpussCat = flossy.changeTo_Cat(whiskerLength: 13.75); + var bagpussCat = flossy.changeToCat(whiskerLength: 13.75); expect(bagpussCat.whiskerLength, 13.75); expect(bagpussCat.runtimeType, Cat); @@ -176,13 +176,13 @@ Sometimes you might want to turn a super class into a subclass (Pet into a Cat) ### Convert object to Json -In order to convert the object to Json specify the `generateJson`. +In order to convert the object to Json specify the `generateJson`. @Morphy(generateJson: true) abstract class $Pet { Add the dev dependency to the json_serializable package - + dart pub add json_serializable --dev Add the part file, .g is required by the json_serializable package used internally by `morphy` @@ -190,9 +190,9 @@ Add the part file, .g is required by the json_serializable package used internal part 'Pets.g.dart'; part 'Pets.morphy.dart'; -Build the generated files then use the toJson_2 method to generate the JSON +Build the generated files then use the toJsonCustom method to generate the JSON - var json = flossy.toJson_2({}); + var json = flossy.toJsonCustom({}); expect(json, {'name': 'Flossy', 'age': 5, '_className_': 'Pet'}); ### Convert Json to Object @@ -209,19 +209,19 @@ Use the factory method `Pet.fromJson()` to create the new object. ### Json to Object Polymorphism Unlike other json conversions you can convert subtypes using the super types toJson and fromJson functions. - + The subtypes must be specified in the explicitSubTypes and in the correct order for this to work. @Morphy(generateJson: true, explicitSubTypes: [$Z, $Y]) abstract class $X { String get val; } - + @Morphy(generateJson: true, explicitSubTypes: [$Z]) abstract class $Y implements $X { int get valY; } - + @Morphy(generateJson: true) abstract class $Z implements $Y { double get valZ; @@ -235,7 +235,7 @@ The subtypes must be specified in the explicitSubTypes and in the correct order We can then just convert our list of X objects to JSON preserving their original type. - var resultInJsonFormat = xObjects.map((e) => e.toJson_2({})).toList(); + var resultInJsonFormat = xObjects.map((e) => e.toJsonCustom({})).toList(); var expectedJson = [ {'val': 'x', '_className_': 'X'}, @@ -267,10 +267,10 @@ We also allow multiple inheritance. ### Custom Constructors -To allow custom constructors you can simply create a publicly accessible factory function that calls the constructor (ie just a method that calls the default constructor). +To allow custom constructors you can simply create a publicly accessible factory function that calls the constructor (ie just a method that calls the default constructor). If you'd like to hide the automatic constructor set the `hidePublicConstructor` on the Morphy annotation to true. -If you do hide the default constructor, -then in order for the custom factory function (A_FactoryFunction in the example below) to be able to call the hidden (or private) default constructor, +If you do hide the default constructor, +then in order for the custom factory function (A_FactoryFunction in the example below) to be able to call the hidden (or private) default constructor, your factory function should live in the same file you defined your class. @Morphy(hidePublicConstructor: true) @@ -304,7 +304,7 @@ Optional parameters can be specified using the ? keyword on the getter property. ### Comments Comments are copied from the class definition to the generated class -and for ease of use copied to the constructor too. +and for ease of use copied to the constructor too. ### Constant Constructor @@ -313,7 +313,7 @@ Just define a blank const constructor in your class definition file const $A(); Then call the const constructor using the named constructor ```constant`` - + var a = A.constant(); ### Private Getters @@ -324,17 +324,17 @@ If we start our property with an underscore then we make the getter private but @morphy abstract class $$Pet { int get _ageInYears; - + String get name; } - + @morphy abstract class $Cat implements $$Pet {} - + @morphy - abstract class $Dog implements $$Pet {} + abstract class $Dog implements $$Pet {} - extension Pet_E on Pet { + extension PetE on Pet { int age() => switch (this) { Cat() => _ageInYears * 7, Dog() => _ageInYears * 5, @@ -343,7 +343,7 @@ If we start our property with an underscore then we make the getter private but var cat = Cat(name: "Tom", ageInYears: 3); var dog = Dog(name: "Rex", ageInYears: 3); - + expect(cat.age(), 21); expect(dog.age(), 15); @@ -358,4 +358,4 @@ a kind of two step route back. You must go to the generated and then you can cl $ version of the class which will be next to it. Sometimes one class needs to be built before another. -In that scenario use morphy2 as the annotation in one class and morphy in the other. \ No newline at end of file +In that scenario use morphy2 as the annotation in one class and morphy in the other. diff --git a/morphy/build.yaml b/morphy/build.yaml index eb88bf7..49379dd 100644 --- a/morphy/build.yaml +++ b/morphy/build.yaml @@ -9,15 +9,25 @@ builders: target: ":morphy" import: "package:morphy/morphyBuilder.dart" builder_factories: ["morphyBuilder"] - build_extensions: {".dart": [".morphy.part"]} + build_extensions: { ".dart": [".morphy.dart"] } auto_apply: dependents build_to: source - runs_before: ["json_serializable:json_serializable", "typedef_for_fn_generator:typedef_for_fn","mock_creator_generator:mock_creator","copy_with_e_generator:copy_with_e", "copy_with_e_generator:copy_with_e","memoizer_generator:memoizer_generator"] + applies_builders: ["source_gen|combining_builder"] + runs_before: ["json_serializable:json_serializable"] morphy2: target: ":morphy2_generator" import: "package:morphy/morphy2Builder.dart" builder_factories: ["morphy2Builder"] - build_extensions: {".dart": [".morphy2.part"]} + build_extensions: { ".dart": [".morphy2.dart"] } auto_apply: dependents build_to: source - runs_before: ["json_serializable|json_serializable", "morphy:morphy", "typedef_for_fn_generator:typedef_for_fn","mock_creator_generator:mock_creator","copy_with_e_generator:copy_with_e", "copy_with_e_generator:copy_with_e","memoizer_generator:memoizer_generator"] + runs_before: + [ + "json_serializable|json_serializable", + "morphy:morphy", + "typedef_for_fn_generator:typedef_for_fn", + "mock_creator_generator:mock_creator", + "copy_with_e_generator:copy_with_e", + "copy_with_e_generator:copy_with_e", + "memoizer_generator:memoizer_generator", + ] diff --git a/morphy/lib/morphy2Builder.dart b/morphy/lib/morphy2Builder.dart index d044ba4..ba7f178 100644 --- a/morphy/lib/morphy2Builder.dart +++ b/morphy/lib/morphy2Builder.dart @@ -4,7 +4,8 @@ import 'package:source_gen/source_gen.dart'; import 'package:morphy_annotation/morphy_annotation.dart'; Builder morphy2Builder(BuilderOptions options) => // - PartBuilder([MorphyGenerator()], '.morphy2.dart', + PartBuilder( + [MorphyGenerator()], '.morphy2.dart', // Keep as .morphy2.dart header: ''' // ignore_for_file: UNNECESSARY_CAST // ignore_for_file: type=lint diff --git a/morphy/lib/morphyBuilder.dart b/morphy/lib/morphyBuilder.dart index db4e769..205cdc1 100644 --- a/morphy/lib/morphyBuilder.dart +++ b/morphy/lib/morphyBuilder.dart @@ -8,4 +8,4 @@ Builder morphyBuilder(BuilderOptions options) => // header: ''' // ignore_for_file: UNNECESSARY_CAST // ignore_for_file: type=lint - '''); +'''); diff --git a/morphy/lib/src/MorphyGenerator.dart b/morphy/lib/src/MorphyGenerator.dart index 82a0996..41087ee 100644 --- a/morphy/lib/src/MorphyGenerator.dart +++ b/morphy/lib/src/MorphyGenerator.dart @@ -1,129 +1,179 @@ import 'dart:async'; - import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/type.dart'; -////import 'package:analyzer_models/analyzer_models.dart'; import 'package:build/src/builder/build_step.dart'; import 'package:dartx/dartx.dart'; import 'package:morphy/src/common/GeneratorForAnnotationX.dart'; import 'package:morphy/src/common/NameType.dart'; import 'package:morphy/src/common/classes.dart'; import 'package:morphy/src/common/helpers.dart'; -import 'package:morphy/src/common/trees.dart'; import 'package:morphy/src/createMorphy.dart'; import 'package:morphy/src/helpers.dart'; import 'package:morphy_annotation/morphy_annotation.dart'; import 'package:source_gen/source_gen.dart'; +import 'package:path/path.dart' as p; + +class MorphyGenerator + extends GeneratorForAnnotationX { + static final Map _allAnnotatedClasses = {}; + static final Map> _allImplementedInterfaces = {}; + static Map get allAnnotatedClasses => + _allAnnotatedClasses; + + @override + TypeChecker get typeChecker => TypeChecker.fromRuntime(TValueT); + + @override + FutureOr generate(LibraryReader library, BuildStep buildStep) async { + // First pass: collect all annotated classes + for (var annotatedElement in library.annotatedWith(typeChecker)) { + if (annotatedElement.element is ClassElement) { + var element = annotatedElement.element as ClassElement; + _allAnnotatedClasses[element.name] = element; + _allImplementedInterfaces[element.name] = element.interfaces; + } + } + + return super.generate(library, buildStep); + } -//List getAllFields(List interfaceTypes, ClassElement element) { -// var superTypeFields = interfaceTypes // -// .where((x) => x.element.name != "Object") -// .flatMap((st) => st.element.fields.map((f) => // -// NameTypeClassComment(f.name, f.type.toString(), st.element.name, comment: f.getter.documentationComment))) -// .toList(); -// var classFields = element.fields.map((f) => // -// NameTypeClassComment(f.name, f.type.toString(), element.name, comment: f.getter.documentationComment)).toList(); -// -// //distinct, will keep classFields over superTypeFields -// return (classFields + superTypeFields).distinctBy((x) => x.name).toList(); -//} - -class MorphyGenerator extends GeneratorForAnnotationX { @override FutureOr generateForAnnotatedElement( - Element ce, + Element element, ConstantReader annotation, BuildStep buildStep, List allClasses, ) { var sb = StringBuffer(); -// sb.writeln("//RULES: you must use implements, not extends"); - - if (ce is! ClassElement) { + if (element is! ClassElement) { throw Exception("not a class"); } - var hasConstConstructor = ce.constructors.any((e) => e.isConst); + _allAnnotatedClasses[element.name] = element; + + // Verify required imports + verifyRequiredImports(element, buildStep); + + var hasConstConstructor = element.constructors.any((e) => e.isConst); + var isAbstract = element.name.startsWith("\$\$"); + var nonSealed = annotation.read('nonSealed').boolValue; + + // If this is implementing a sealed class, verify it's in the same library + for (var interface in element.interfaces) { + var interfaceName = interface.element.name; + // Only check if: + // 1. The interface is a sealed class (starts with $$) + // 2. This class is a concrete implementation (doesn't start with $) + if (interfaceName.startsWith("\$\$") && !element.name.startsWith("\$")) { + var sealedLibrary = interface.element.library; + var implementationLibrary = element.library; + + // Check if they're in the same library (includes part files) + var isSameLibrary = sealedLibrary == implementationLibrary || + implementationLibrary.parts + .any((part) => part.library == sealedLibrary) || + sealedLibrary.parts + .any((part) => part.library == implementationLibrary); + + if (!isSameLibrary) { + throw Exception( + 'Class ${element.name} must be in the same library as its sealed superclass ${interfaceName}. ' + + 'Either move it to the same library or use "part of" directive.'); + } + } + } - if (ce.supertype?.element.name != "Object") { + if (element.supertype?.element.name != "Object") { throw Exception("you must use implements, not extends"); } - var docComment = ce.documentationComment; + var docComment = element.documentationComment; - var isAbstract = ce.name.startsWith("\$\$"); - var allFields = getAllFields(ce.allSupertypes, ce).where((x) => x.name != "hashCode").toList(); + // Collect all interfaces including the inheritance chain + var allInterfaces = []; + var processedInterfaces = {}; - var className = ce.name; - var interfaces = ce.interfaces - .map((e) => // - InterfaceWithComment( - e.element.name, - e.typeArguments.map(typeToString).toList(), - e.element.typeParameters.map((x) => x.name).toList(), - e.element.fields.map((e) => NameType(e.name, typeToString(e.type))).toList(), - comment: e.element.documentationComment, - )) // - .toList(); + void addInterface(InterfaceType interface) { + if (processedInterfaces.contains(interface.element.name)) return; + processedInterfaces.add(interface.element.name); + allInterfaces.add(interface); + + // Add super interfaces + interface.element.interfaces.forEach(addInterface); + interface.element.allSupertypes + .where((t) => t.element.name != 'Object') + .forEach(addInterface); + } -// interfaces.forEach((element) { -// sb.writeln("//interfacefields: ${element.fields.toString()})"); -// }); + // Process all interfaces + element.interfaces.forEach(addInterface); - var classGenerics = ce.typeParameters - .map((e) { - final bound = e.bound; - return NameTypeClassComment(e.name, bound == null ? null : typeToString(bound), null); - }) // - .toList(); + var interfaces = allInterfaces.map((e) { + var interfaceName = e.element.name; + // For $$-prefixed interfaces, use the non-sealed interface class name + var implementedName = interfaceName.startsWith("\$\$") + ? interfaceName.replaceAll("\$\$", "") + : interfaceName.replaceAll("\$", ""); + return InterfaceWithComment( + implementedName, + e.typeArguments.map(typeToString).toList(), + e.element.typeParameters.map((x) => x.name).toList(), + e.element.fields + .map((e) => NameType(e.name, typeToString(e.type))) + .toList(), + comment: e.element.documentationComment, + ); + }).toList(); + + // Get all fields including those from the entire inheritance chain + var allFields = getAllFieldsIncludingSubtypes(element); var allFieldsDistinct = getDistinctFields(allFields, interfaces); - //if I want to create a copywith from an annotation passed in I could use this + var classGenerics = element.typeParameters.map((e) { + final bound = e.bound; + return NameTypeClassComment( + e.name, bound == null ? null : typeToString(bound), null); + }).toList(); + var typesExplicit = []; if (!annotation.read('explicitSubTypes').isNull) { - typesExplicit = annotation - .read('explicitSubTypes') // - .listValue - .map((x) { + typesExplicit = annotation.read('explicitSubTypes').listValue.map((x) { if (x.toTypeValue()?.element is! ClassElement) { throw Exception("each type for the copywith def must all be classes"); } var el = x.toTypeValue()?.element as ClassElement; + _allAnnotatedClasses[el.name] = el; return Interface.fromGenerics( el.name, - el.typeParameters // - .map((TypeParameterElement x) { - final bound = x.bound; - return NameType(x.name, bound == null ? null : typeToString(bound)); - }) + el.typeParameters.map((TypeParameterElement x) { + final bound = x.bound; + return NameType(x.name, bound == null ? null : typeToString(bound)); + }).toList(), + getAllFieldsIncludingSubtypes(el) + .where((x) => x.name != "hashCode") .toList(), - getAllFields(el.allSupertypes, el).where((x) => x.name != "hashCode").toList(), true, ); }).toList(); } - //all ValueT interfaces of the class, not just those specified in the implements list - // we need the ones that are inherited by the implements list - var allValueTInterfaces = flatten(ce.interfaces, (x) => x.interfaces) // - .map( - (e) { - return Interface.fromGenerics( + // Process all interfaces including the inheritance chain + var allValueTInterfaces = allInterfaces + .map((e) => Interface.fromGenerics( e.element.name, - e.element.typeParameters // - .map((TypeParameterElement x) { - final bound = x.bound; - return NameType(x.name, bound == null ? null : typeToString(bound)); - }) + e.element.typeParameters.map((TypeParameterElement x) { + final bound = x.bound; + return NameType( + x.name, bound == null ? null : typeToString(bound)); + }).toList(), + getAllFieldsIncludingSubtypes(e.element as ClassElement) + .where((x) => x.name != "hashCode") .toList(), - getAllFields(e.element.allSupertypes, e.element as ClassElement).where((x) => x.name != "hashCode").toList(), - ); - }, - ) + )) .union(typesExplicit) .distinctBy((element) => element.interfaceName) .toList(); @@ -131,7 +181,7 @@ class MorphyGenerator extends GeneratorForAnnotationX extends GeneratorForAnnotationX getAllFieldsIncludingSubtypes( + ClassElement element) { + var fields = []; + var processedTypes = {}; - // var isOutputCommented = false; - // - // var output = isOutputCommented // - // ? sb.toString().replaceAll("\n", "\n//") - // : sb.toString(); + void addFields(ClassElement element) { + if (processedTypes.contains(element.name)) return; + processedTypes.add(element.name); - // return output; + fields.addAll(getAllFields(element.allSupertypes, element) + .where((x) => x.name != "hashCode")); - return sb.toString(); + // Process interfaces + for (var interface in element.interfaces) { + if (_allAnnotatedClasses.containsKey(interface.element.name)) { + addFields(_allAnnotatedClasses[interface.element.name]!); + } + } + } + + addFields(element); + return fields.distinctBy((f) => f.name).toList(); + } + + List getAllImplementedInterfaces(ClassElement element) { + var interfaces = []; + var processedInterfaces = {}; + var queue = element.interfaces.toList(); + + while (queue.isNotEmpty) { + var current = queue.removeAt(0); + if (processedInterfaces.contains(current.element.name)) continue; + processedInterfaces.add(current.element.name); + + interfaces.add(current); + queue.addAll(current.element.interfaces); + + if (_allImplementedInterfaces.containsKey(current.element.name)) { + queue.addAll(_allImplementedInterfaces[current.element.name]!); + } + } + + return interfaces; + } + + void verifyRequiredImports(ClassElement element, BuildStep buildStep) { + var sourceLibrary = element.library; + var requiredTypes = collectRequiredTypes(element); + var missingImports = {}; + + var sourceUri = sourceLibrary.source.uri; + + // Get the package name from the build step + var packageName = buildStep.inputId.package; + var packagePrefix = 'package:$packageName/'; + + for (var typeElement in requiredTypes) { + var targetLibrary = typeElement.library; + if (targetLibrary != null && + targetLibrary != sourceLibrary && + !sourceLibrary.libraryImports + .any((import) => import.importedLibrary == targetLibrary)) { + var targetUri = targetLibrary.source.uri; + + String importUri; + + if (sourceUri.scheme == 'package' && targetUri.scheme == 'package') { + if (sourceUri.toString().startsWith(packagePrefix) && + targetUri.toString().startsWith(packagePrefix)) { + // Both URIs are in the same package, compute relative path + var sourcePath = sourceUri.path.substring( + packageName.length + 1); // Remove 'package:' and package name + var targetPath = targetUri.path.substring(packageName.length + 1); + + var relativePath = + p.relative(targetPath, from: p.dirname(sourcePath)); + // Ensure the relative path uses forward slashes + importUri = relativePath.replaceAll('\\', '/'); + } else { + // Different packages, cannot compute relative path + importUri = targetUri.toString(); + } + } else if (sourceUri.scheme == targetUri.scheme && + sourceUri.scheme == 'file') { + // Both URIs are file URIs + var sourcePath = sourceUri.toFilePath(); + var targetPath = targetUri.toFilePath(); + + var relativePath = + p.relative(targetPath, from: p.dirname(sourcePath)); + // Ensure the relative path uses forward slashes + importUri = relativePath.replaceAll('\\', '/'); + } else { + // Different schemes or cannot compute relative path + importUri = targetUri.toString(); + } + + missingImports.add(importUri); + } + } + + if (missingImports.isNotEmpty) { + var imports = + missingImports.map((importUri) => "import '$importUri';").join('\n'); + throw Exception(''' + Missing required imports in ${sourceLibrary.source.uri}: +$imports + '''); + } + } + + Set collectRequiredTypes(ClassElement element) { + var types = {}; + + // Add implemented interfaces and their inheritance chain + for (var interface in element.interfaces) { + if (interface.element is ClassElement) { + types.add(interface.element as ClassElement); + } + } + + // Add supertypes + for (var supertype in element.allSupertypes) { + if (supertype.element is ClassElement) { + types.add(supertype.element as ClassElement); + } + } + + // Add explicit subtypes from annotation + var annotation = typeChecker.firstAnnotationOf(element); + if (annotation != null) { + var reader = ConstantReader(annotation); + if (!reader.read('explicitSubTypes').isNull) { + var subtypes = reader.read('explicitSubTypes').listValue; + for (var type in subtypes) { + if (type.toTypeValue()?.element is ClassElement) { + types.add(type.toTypeValue()!.element as ClassElement); + } + } + } + } + + return types; } } diff --git a/morphy/lib/src/common/GeneratorForAnnotationX.dart b/morphy/lib/src/common/GeneratorForAnnotationX.dart index 98b1ff4..1ec11ac 100644 --- a/morphy/lib/src/common/GeneratorForAnnotationX.dart +++ b/morphy/lib/src/common/GeneratorForAnnotationX.dart @@ -42,26 +42,9 @@ abstract class GeneratorForAnnotationX extends Generator { @override FutureOr generate(LibraryReader library, BuildStep buildStep) async { - const typedefs = - """ -typedef __String = String; -typedef __Object = Object; -// ignore: unused_element -typedef __List = List; -typedef __Map = Map; -typedef __Never = Never; -typedef __Type = Type; -typedef __int = int; -typedef __bool = bool; -const __hashObjects = hashObjects; -const __identical = identical; -""" - ; final values = Set(); - var classElements = library.allElements // - .whereType() - .toList(); + var classElements = library.allElements.whereType().toList(); for (var annotatedElement in library.annotatedWith(typeChecker)) { final generatedValue = generateForAnnotatedElement( @@ -76,10 +59,7 @@ const __identical = identical; } } - return [ - if (this is MorphyGenerator && values.isNotEmpty) typedefs, - ...values - ].join('\n\n'); + return values.join('\n\n'); } /// Implement to return source code to generate for [element]. diff --git a/morphy/lib/src/common/classes.dart b/morphy/lib/src/common/classes.dart index 0da7c38..b5958ae 100644 --- a/morphy/lib/src/common/classes.dart +++ b/morphy/lib/src/common/classes.dart @@ -16,8 +16,11 @@ class Interface { List genericName, this.fields, [ this.isExplicitSubType = false, - ]) : assert(genericExtends.length == genericName.length, "typeArgs must have same length as typeParams"), - typeParams = genericName.mapIndexed((i, x) => NameType(x, genericExtends[i])).toList() {} + ]) : assert(genericExtends.length == genericName.length, + "typeArgs must have same length as typeParams"), + typeParams = genericName + .mapIndexed((i, x) => NameType(x, genericExtends[i])) + .toList() {} Interface.fromGenerics( this.interfaceName, @@ -26,7 +29,8 @@ class Interface { this.isExplicitSubType = false, ]); - toString() => "${this.interfaceName.toString()}|${this.typeParams.toString()}|${this.fields.toString()}"; + toString() => + "${this.interfaceName.toString()}|${this.typeParams.toString()}|${this.fields.toString()}"; } class InterfaceWithComment extends Interface { @@ -40,7 +44,8 @@ class InterfaceWithComment extends Interface { this.comment, }) : super(type, typeArgsTypes, typeParamsNames, fields); - toString() => "${this.interfaceName.toString()}|${this.typeParams.toString()}|${this.fields.toString()}"; + toString() => + "${this.interfaceName.toString()}|${this.typeParams.toString()}|${this.fields.toString()}"; } class ClassDef { diff --git a/morphy/lib/src/createMorphy.dart b/morphy/lib/src/createMorphy.dart index 0f7ace1..d4e628c 100644 --- a/morphy/lib/src/createMorphy.dart +++ b/morphy/lib/src/createMorphy.dart @@ -16,6 +16,7 @@ String createMorphy( List explicitForJson, bool nonSealed, bool explicitToJson, + bool generateCompareTo, ) { //recursively go through otherClasses and get my fieldnames & @@ -24,26 +25,33 @@ String createMorphy( sb.write(getClassComment(interfacesFromImplements, classComment)); - //move this into a helper class! if (generateJson) { sb.writeln(createJsonSingleton(classNameTrim, classGenerics)); - sb.writeln(createJsonHeader(className, classGenerics, hidePublicConstructor, explicitToJson)); + sb.writeln(createJsonHeader(className, classGenerics, hidePublicConstructor, + explicitToJson, generateCompareTo)); } - sb.write(getClassDefinition(isAbstract: isAbstract, nonSealed: nonSealed, className: className)); + sb.write(getClassDefinition( + isAbstract: isAbstract, nonSealed: nonSealed, className: className)); if (classGenerics.isNotEmpty) { sb.write(getClassGenerics(classGenerics)); } - sb.write(" extends ${className}"); + // Handle extends and implements + if (!isAbstract || (isAbstract && nonSealed)) { + // For concrete classes or non-sealed abstract classes ($-prefixed) + sb.write(" extends ${className}"); + } if (classGenerics.isNotEmpty) { sb.write(getExtendsGenerics(classGenerics)); } + if (interfacesFromImplements.isNotEmpty) { sb.write(getImplements(interfacesFromImplements, className)); } + sb.writeln(" {"); if (isAbstract) { sb.writeln(getPropertiesAbstract(allFields)); @@ -56,6 +64,7 @@ String createMorphy( if (allFields.isEmpty) { if (!hidePublicConstructor) { sb.writeln("${classNameTrim}();"); + sb.writeln('\n'); } sb.writeln("${classNameTrim}._();"); } else { @@ -63,31 +72,36 @@ String createMorphy( if (!hidePublicConstructor) { sb.writeln("${classNameTrim}({"); sb.writeln(getConstructorRows(allFields)); - sb.writeln("}) ${getInitialiser(allFields)};"); + sb.writeln("}) ${getInitializer(allFields)};"); } //the json needs a public constructor, we add this if public constructor is hidden if (hidePublicConstructor && generateJson) { sb.writeln("${classNameTrim}.forJsonDoNotUse({"); sb.writeln(getConstructorRows(allFields)); - sb.writeln("}) ${getInitialiser(allFields)};"); + sb.writeln("}) ${getInitializer(allFields)};"); } //we always want to write a private constructor (just a duplicate) sb.writeln("${classNameTrim}._({"); sb.writeln(getConstructorRows(allFields)); - sb.writeln("}) ${getInitialiser(allFields)};"); + sb.writeln("}) ${getInitializer(allFields)};"); + sb.writeln('\n'); if (hasConstContructor) { sb.writeln("const ${classNameTrim}.constant({"); sb.writeln(getConstructorRows(allFields)); - sb.writeln("}) ${getInitialiser(allFields)};"); + sb.writeln("}) ${getInitializer(allFields)};"); + sb.writeln('\n'); } sb.writeln(getToString(allFields, classNameTrim)); } + sb.writeln('\n'); sb.writeln(getHashCode(allFields)); + sb.writeln('\n'); sb.writeln(getEquals(allFields, classNameTrim)); + sb.writeln('\n'); } // var interfacesX = [ @@ -122,14 +136,31 @@ String createMorphy( sb.writeln(generateFromJsonHeader(className)); sb.writeln(generateFromJsonBody(className, classGenerics, explicitForJson)); sb.writeln(generateToJson(className, classGenerics)); + sb.writeln(generateToJsonLean(className)); } sb.writeln("}"); + if (!isAbstract && !className.startsWith('\$\$') && generateCompareTo) { + // Create a list of all known classes from the interfaces + var knownClasses = [ + ...interfacesAllInclSubInterfaces + .map((i) => i.interfaceName.replaceAll("\$", "")), + classNameTrim, + ].toSet().toList(); + + sb.writeln(generateCompareExtension( + isAbstract, + className, + classNameTrim, + allFields, + interfacesAllInclSubInterfaces, // Pass all known interfaces + knownClasses, // Pass all known classes + generateCompareTo, + )); + } + sb.writeln("extension ${className}changeToE on ${className} {"); - sb.writeln(); - sb.writeln("extension ${className}_changeTo_E on ${className} {"); - - if(!isAbstract){ + if (!isAbstract) { sb.writeln( getCopyWith( classFields: allFields, @@ -163,7 +194,3 @@ String createMorphy( // return commentEveryLine(sb.toString()); return sb.toString(); } - -String commentEveryLine(String multilineString) { - return multilineString.split('\n').map((line) => '//' + line).join('\n'); -} diff --git a/morphy/lib/src/helpers.dart b/morphy/lib/src/helpers.dart index 76d37cb..7418f30 100644 --- a/morphy/lib/src/helpers.dart +++ b/morphy/lib/src/helpers.dart @@ -36,7 +36,9 @@ List getDistinctFields( List fieldsRaw, List interfaces, ) { - var fields = fieldsRaw.map((f) => NameTypeClassComment(f.name, f.type, f.className?.replaceAll("\$", ""), comment: f.comment)); + var fields = fieldsRaw.map((f) => NameTypeClassComment( + f.name, f.type, f.className?.replaceAll("\$", ""), + comment: f.comment)); var interfaces2 = interfaces // .map((x) => Interface.fromGenerics( @@ -49,11 +51,16 @@ List getDistinctFields( // return Interface2(interface.type.replaceAll("\$", ""), result); // }).toList(); - var sortedFields = fields.sortedBy((element) => element.className ?? "").toList(); - var distinctFields = sortedFields.distinctBy((element) => element.name).toList(); + var sortedFields = + fields.sortedBy((element) => element.className ?? "").toList(); + var distinctFields = + sortedFields.distinctBy((element) => element.name).toList(); var adjustedFields = distinctFields.map((classField) { - var i = interfaces2.where((x) => x.interfaceName == classField.className).take(1).toList(); + var i = interfaces2 + .where((x) => x.interfaceName == classField.className) + .take(1) + .toList(); if (i.length > 0) { var paramNameType = i[0] .typeParams @@ -62,23 +69,35 @@ List getDistinctFields( .toList(); if (paramNameType.length > 0) { var name = removeDollarsFromPropertyType(paramNameType[0].type!); - return NameTypeClassComment(classField.name, name, null, comment: classField.comment); + return NameTypeClassComment(classField.name, name, null, + comment: classField.comment); } } var type = removeDollarsFromPropertyType(classField.type!); - return NameTypeClassComment(classField.name, type, null, comment: classField.comment); + return NameTypeClassComment(classField.name, type, null, + comment: classField.comment); }).toList(); return adjustedFields; } -String getClassDefinition({required bool isAbstract, required bool nonSealed, required String className}) { +String getClassDefinition({ + required bool isAbstract, + required bool nonSealed, + required String className, +}) { var _className = className.replaceAll("\$", ""); - var abstract = isAbstract ? (nonSealed ? "abstract " : "sealed ") : ""; + if (isAbstract) { + if (!nonSealed) { + // Just generate a sealed class for $$-prefixed classes + return "sealed class $_className"; + } + return "abstract class $_className"; + } - return "${abstract}class $_className"; + return "class $_className"; } String getClassGenerics(List generics) { @@ -127,14 +146,94 @@ String getImplements(List interfaces, String className) { return " implements $types"; } -String getEnumPropertyList(List fields, String className) { - if (fields.isEmpty) // - return ''; +String getEnumPropertyList( + List fields, String className) { + if (fields.isEmpty) return ''; + + String classNameTrim = '${className.replaceAll("\$", "")}'; + String enumName = '${classNameTrim}\$'; + + var sb = StringBuffer(); + + // Generate enum + sb.writeln("enum $enumName {"); + sb.writeln(fields + .map((e) => e.name.startsWith("_") ? e.name.substring(1) : e.name) + .join(",")); + sb.writeln("}\n"); - var first = "enum ${className.replaceAll("\$", "")}\$ {"; - var last = fields.map((e) => // - e.name.startsWith("_") ? e.name.substring(1) : e.name).join(",") + "}"; - return first + last; + // Generate patch class + sb.writeln("class ${classNameTrim}Patch {"); + sb.writeln(" final Map<$enumName, dynamic> _patch = {};"); + sb.writeln(); + + // Static factory methods + sb.writeln( + " static ${classNameTrim}Patch create([Map? diff]) {"); + sb.writeln(" final patch = ${classNameTrim}Patch();"); + sb.writeln(" if (diff != null) {"); + sb.writeln(" diff.forEach((key, value) {"); + sb.writeln(" try {"); + sb.writeln( + " final enumValue = $enumName.values.firstWhere((e) => e.name == key);"); + sb.writeln(" if (value is Function) {"); + sb.writeln(" patch._patch[enumValue] = value();"); + sb.writeln(" } else {"); + sb.writeln(" patch._patch[enumValue] = value;"); + sb.writeln(" }"); + sb.writeln(" } catch (_) {}"); + sb.writeln(" });"); + sb.writeln(" }"); + sb.writeln(" return patch;"); + sb.writeln(" }"); + sb.writeln(); + + // Convert to map method + sb.writeln(" Map<$enumName, dynamic> toPatch() => Map.from(_patch);"); + sb.writeln(); + + // Add toJson method + sb.writeln(" Map toJson() {"); + sb.writeln(" final json = {};"); + sb.writeln(" _patch.forEach((key, value) {"); + sb.writeln(" if (value != null) {"); + sb.writeln(" if (value is DateTime) {"); + sb.writeln(" json[key.name] = value.toIso8601String();"); + sb.writeln(" } else if (value is List) {"); + sb.writeln( + " json[key.name] = value.map((e) => e?.toJson ?? e).toList();"); + sb.writeln(" } else {"); + sb.writeln(" json[key.name] = value?.toJson ?? value;"); + sb.writeln(" }"); + sb.writeln(" }"); + sb.writeln(" });"); + sb.writeln(" return json;"); + sb.writeln(" }"); + sb.writeln(); + + // Add fromJson factory + sb.writeln( + " static ${classNameTrim}Patch fromJson(Map json) {"); + sb.writeln(" return create(json);"); + sb.writeln(" }"); + sb.writeln(); + + // Generate with methods + for (var field in fields) { + var name = + field.name.startsWith("_") ? field.name.substring(1) : field.name; + var type = getDataTypeWithoutDollars(field.type ?? "dynamic"); + + sb.writeln(" ${classNameTrim}Patch with$name($type value) {"); + sb.writeln(" _patch[$enumName.$name] = value;"); + sb.writeln(" return this;"); + sb.writeln(" }"); + sb.writeln(); + } + + sb.writeln("}"); + + return sb.toString(); } /// remove dollars from the dataType except for function types @@ -168,7 +267,8 @@ String getPropertiesAbstract(List fields) => // String getConstructorRows(List fields) => // fields .map((e) { - var required = e.type!.substring(e.type!.length - 1) == "?" ? "" : "required "; + var required = + e.type!.substring(e.type!.length - 1) == "?" ? "" : "required "; var thisOrType = e.name.startsWith("_") ? "${e.type} " : "this."; var propertyName = e.name[0] == '_' ? e.name.substring(1) : e.name; return "$required$thisOrType$propertyName,"; @@ -176,7 +276,7 @@ String getConstructorRows(List fields) => // .join("\n") .trim(); -String getInitialiser(List fields) { +String getInitializer(List fields) { var result = fields .where((e) => e.name.startsWith('_')) .map((e) { @@ -191,11 +291,13 @@ String getInitialiser(List fields) { String getToString(List fields, String className) { if (fields.isEmpty) { - return """__String toString() => "($className-)"""; + return """String toString() => "($className-)"""; } - var items = fields.map((e) => "${e.name}:\${${e.name}.toString()}").joinToString(separator: "|"); - return """__String toString() => "($className-$items)";"""; + var items = fields + .map((e) => "${e.name}:\${${e.name}.toString()}") + .joinToString(separator: "|"); + return """String toString() => "($className-$items)";"""; } String getHashCode(List fields) { @@ -203,19 +305,22 @@ String getHashCode(List fields) { return ""; } - var items = fields.map((e) => "${e.name}.hashCode").joinToString(separator: ", "); - return """__int get hashCode => __hashObjects([$items]);"""; + var items = + fields.map((e) => "${e.name}.hashCode").joinToString(separator: ", "); + return """int get hashCode => hashObjects([$items]);"""; } String getEquals(List fields, String className) { var sb = StringBuffer(); - sb.write("__bool operator ==(__Object other) => __identical(this, other) || other is $className && runtimeType == other.runtimeType"); + sb.write( + "bool operator ==(Object other) => identical(this, other) || other is $className && runtimeType == other.runtimeType"); sb.writeln(fields.isEmpty ? "" : " &&"); sb.write(fields.map((e) { - if ((e.type!.characters.take(5).string == "List<" || e.type!.characters.take(4).string == "Set<")) { + if ((e.type!.characters.take(5).string == "List<" || + e.type!.characters.take(4).string == "Set<")) { //todo: hack here, a nullable entry won't compare properly to an empty list if (e.type!.characters.last == "?") { return "(${e.name}??[]).equalUnorderedD(other.${e.name}??[])"; @@ -232,16 +337,20 @@ String getEquals(List fields, String className) { return sb.toString(); } -String createJsonHeader(String className, List classGenerics, bool privateConstructor, bool explicitToJson) { +String createJsonHeader(String className, List classGenerics, + bool privateConstructor, bool explicitToJson, bool generateCompareTo) { var sb = StringBuffer(); if (!className.startsWith("\$\$")) { - var jsonConstructorName = privateConstructor ? "constructor: 'forJsonDoNotUse'" : ""; + var jsonConstructorName = + privateConstructor ? "constructor: 'forJsonDoNotUse'" : ""; if (classGenerics.length > 0) // - sb.writeln("@JsonSerializable(explicitToJson: $explicitToJson, genericArgumentFactories: true, $jsonConstructorName)"); + sb.writeln( + "@JsonSerializable(explicitToJson: $explicitToJson, genericArgumentFactories: true, $jsonConstructorName)"); else - sb.writeln("@JsonSerializable(explicitToJson: $explicitToJson, $jsonConstructorName)"); + sb.writeln( + "@JsonSerializable(explicitToJson: $explicitToJson, $jsonConstructorName)"); } return sb.toString(); @@ -258,33 +367,59 @@ String getCopyWith({ required String className, required bool isClassAbstract, required List interfaceGenerics, - bool isExplicitSubType = false, //for where we specify the explicit subtypes for changeTo + bool isExplicitSubType = false, }) { var sb = StringBuffer(); - var classNameTrimmed = className.replaceAll("\$", ""); var interfaceNameTrimmed = interfaceName.replaceAll("\$", ""); - isExplicitSubType // - ? sb.write("$interfaceNameTrimmed changeTo_${interfaceNameTrimmed}") - : sb.write("$classNameTrimmed copyWith_$interfaceNameTrimmed"); - - if (interfaceGenerics.isNotEmpty) { - var generic = interfaceGenerics // - .map((e) => e.type == null // - ? e.name - : "${e.name} extends ${e.type}") - .joinToString(separator: ", "); - sb.write("<$generic>"); + // var interfaceGenericString = interfaceGenerics // + // .map((e) => e.type == null // + // ? e.name + // : "${e.name} extends ${e.type}") + // .joinToString(separator: ", "); + + var interfaceGenericStringWithExtends = interfaceGenerics // + .map((e) => e.type == null // + ? e.name + : "${e.name} extends ${e.type}") + .joinToString(separator: ", "); + + if (interfaceGenericStringWithExtends.length > 0) { + interfaceGenericStringWithExtends = "<$interfaceGenericStringWithExtends>"; } + var interfaceGenericStringNoExtends = interfaceGenerics // + .map((e) => e.name) + .joinToString(separator: ", "); + + if (interfaceGenericStringNoExtends.length > 0) { + interfaceGenericStringNoExtends = "<$interfaceGenericStringNoExtends>"; + } + + isExplicitSubType // + ? sb.write( + "$interfaceNameTrimmed$interfaceGenericStringNoExtends changeTo$interfaceNameTrimmed$interfaceGenericStringWithExtends") + : sb.write( + "$interfaceNameTrimmed$interfaceGenericStringNoExtends copyWith$interfaceNameTrimmed$interfaceGenericStringWithExtends"); + + // if (interfaceGenerics.isNotEmpty) { + // var generic = interfaceGenerics // + // .map((e) => e.type == null // + // ? e.name + // : "${e.name} extends ${e.type}") + // .joinToString(separator: ", "); + // sb.write("<$generic>"); + // } + sb.write("("); //where property name of interface is the same as the one in the class //use the type of the class var fieldsForSignature = classFields // - .where((element) => interfaceFields.map((e) => e.name).contains(element.name)); + .where((element) => + interfaceFields.map((e) => e.name).contains(element.name)); // identify fields in the interface not in the class var requiredFields = isExplicitSubType // @@ -299,7 +434,8 @@ String getCopyWith({ sb.writeln(); sb.write(requiredFields.map((e) { - var interfaceType = interfaceFields.firstWhere((element) => element.name == e.name).type; + var interfaceType = + interfaceFields.firstWhere((element) => element.name == e.name).type; return "required ${getDataTypeWithoutDollars(interfaceType!)} ${e.name},\n"; }).join()); @@ -315,7 +451,7 @@ String getCopyWith({ return "${getDataTypeWithoutDollars(interfaceType!)} Function()? $name,\n"; }).join()); - if (fieldsForSignature.isNotEmpty) // + if (fieldsForSignature.isNotEmpty || requiredFields.isNotEmpty) // sb.write("}"); if (isClassAbstract && !isExplicitSubType) { @@ -326,7 +462,10 @@ String getCopyWith({ sb.writeln(") {"); if (isExplicitSubType) { - sb.writeln("return ${getDataTypeWithoutDollars(interfaceName)}._("); + // Use public constructor if changing to a different type + var usePrivateConstructor = interfaceNameTrimmed == classNameTrimmed; + sb.writeln( + "return ${getDataTypeWithoutDollars(interfaceName)}${usePrivateConstructor ? '._' : ''}("); } else { sb.writeln("return $classNameTrimmed._("); } @@ -342,18 +481,27 @@ String getCopyWith({ .map((e) { var name = e.name.startsWith("_") ? e.name.substring(1) : e.name; - var classType = getDataTypeWithoutDollars(classFields.firstWhere((element) => element.name == e.name).type!); + var classType = getDataTypeWithoutDollars( + classFields.firstWhere((element) => element.name == e.name).type!); return "$name: $name == null ? this.${e.name} as $classType : $name() as $classType,\n"; }).join()); var fieldsNotInSignature = classFields // - .where((element) => !interfaceFields.map((e) => e.name).contains(element.name)); + .where((element) => + !interfaceFields.map((e) => e.name).contains(element.name)); sb.write(fieldsNotInSignature // - .map((e) => "${e.name.startsWith('_') ? e.name.substring(1) : e.name }: (this as $classNameTrimmed).${e.name},\n") + .map((e) => + "${e.name.startsWith('_') ? e.name.substring(1) : e.name}: (this as $classNameTrimmed).${e.name},\n") .join()); - sb.writeln(");"); + sb.write(") as $interfaceNameTrimmed$interfaceGenericStringNoExtends;"); + + // if (isExplicitSubType) { + // sb.write(") as $interfaceNameTrimmed;"); + // } else { + // sb.write(") as $interfaceNameTrimmed$interfaceGenericStringNoExtends;"); + // } sb.write("}"); return sb.toString(); @@ -390,84 +538,96 @@ String getConstructorName(String trimmedClassName, bool hasCustomConstructor) { String generateFromJsonHeader(String className) { var _className = "${className.replaceFirst("\$", "")}"; - - return "factory ${_className.replaceFirst("\$", "")}.fromJson(__Map<__String, dynamic> json) {"; + return "factory ${_className.replaceFirst("\$", "")}.fromJson(Map json) {"; } -String generateFromJsonBody(String className, List generics, List interfaces) { - var _class = Interface(className, generics.map((e) => e.type ?? "").toList(), generics.map((e) => e.name).toList(), []); +String generateFromJsonBody( + String className, List generics, List interfaces) { + var _class = Interface(className, generics.map((e) => e.type ?? "").toList(), + generics.map((e) => e.name).toList(), []); var _classes = [...interfaces, _class]; + var _className = className.replaceAll("\$", ""); - var body = _classes // + var body = """if (json['_className_'] == null) { + return _\$${_className}FromJson(json); + } +"""; + + // Add interface checks + var interfaceChecks = _classes .where((c) => !c.interfaceName.startsWith("\$\$")) .mapIndexed((i, c) { var _interfaceName = "${c.interfaceName.replaceFirst("\$", "")}"; - var start = i == 0 ? "if" : "} else if"; var genericTypes = c.typeParams.map((e) => "'_${e.name}_'").join(","); - // var types = c.typeParams.length == 0 ? "" : "<${c.typeParams.map((t) => "dynamic").join(', ')}>"; + var isCurrentClass = _interfaceName == className.replaceAll("\$", ""); + var prefix = i == 0 ? "if" : "} else if"; if (c.typeParams.length > 0) { - return """$start (json['_className_'] == "$_interfaceName") { + return """$prefix (json['_className_'] == "$_interfaceName") { var fn_fromJson = getFromJsonToGenericFn( ${_interfaceName}_Generics_Sing().fns, json, [$genericTypes], - ); - return fn_fromJson(json); -"""; + ); + return fn_fromJson(json);"""; } else { - return """$start (json['_className_'] == "$_interfaceName") { - return _\$${_interfaceName}FromJson(json, ); -"""; + return """$prefix (json['_className_'] == "$_interfaceName") { + return ${isCurrentClass ? "_\$" : ""}${_interfaceName}${isCurrentClass ? "FromJson" : ".fromJson"}(json);"""; } }).join("\n"); - var _className = className.replaceFirst("\$", "").replaceFirst("\$", ""); + body += interfaceChecks; + if (interfaceChecks.isNotEmpty) body += "\n}"; - var end = """ } else { - return _\$${_className}FromJson(json, ); - } - } -"""; + body += """ + throw UnsupportedError("The _className_ '\${json['_className_']}' is not supported by the ${_className}.fromJson constructor."); + }"""; - return body + end; + return body; } String generateToJson(String className, List generics) { if (className.startsWith("\$\$")) { - return "__Map<__String, dynamic> toJson_2([__Map<__Type, __Object? Function(__Never)>? fns]);"; + return "Map toJsonCustom([Map? fns]);"; } var _className = "${className.replaceFirst("\$", "")}"; - var getGenericFn = generics // - .map((e) => " var fn_${e.name} = getGenericToJsonFn(_fns, ${e.name});") - .join("\n"); - - var toJsonParams = generics // - .map((e) => " fn_${e.name} as __Object? Function(${e.name})") - .join(",\n"); - - var recordType = generics // - .map((e) => " data['_${e.name}_'] = ${e.name}.toString();") - .join("\n"); + var getGenericFn = generics.isEmpty + ? "" + : generics + .map((e) => + " var fn_${e.name} = getGenericToJsonFn(_fns, ${e.name});") + .join("\n") + + "\n"; + + var toJsonParams = generics.isEmpty + ? "" + : generics + .map((e) => " fn_${e.name} as Object? Function(${e.name})") + .join(",\n") + + "\n"; + + var recordType = generics.isEmpty + ? "" + : generics + .map((e) => " data['_${e.name}_'] = ${e.name}.toString();") + .join("\n") + + "\n"; var result = """ - // ignore: unused_field - __Map<__Type, __Object? Function(__Never)> _fns = {}; + // ignore: unused_field\n + Map _fns = {}; - __Map<__String, dynamic> toJson_2([__Map<__Type, __Object? Function(__Never)>? fns]){ - this._fns = fns ?? {}; + Map toJsonCustom([Map? fns]){ + _fns = fns ?? {}; return toJson(); } - __Map<__String, dynamic> toJson() { -$getGenericFn - final __Map<__String, dynamic> data = _\$${_className}ToJson(this, -$toJsonParams); - // Adding custom key-value pair - data['_className_'] = '$_className'; -$recordType + Map toJson() { +$getGenericFn final Map data = _\$${_className}ToJson(this${generics.isEmpty ? "" : ",\n$toJsonParams"}); + + data['_className_'] = '$_className';${recordType.isEmpty ? "" : "\n$recordType"} return data; }"""; @@ -475,22 +635,261 @@ $recordType return result; } +String generateToJsonLean(String className) { + if (className.startsWith("\$\$")) { + return ""; + } + + var _className = "${className.replaceFirst("\$", "")}"; + var result = """ + + Map toJsonLean() { + final Map data = _\$${_className}ToJson(this,); + return _sanitizeJson(data); + } + + dynamic _sanitizeJson(dynamic json) { + if (json is Map) { + json.remove('_className_'); + return json..forEach((key, value) { + json[key] = _sanitizeJson(value); + }); + } else if (json is List) { + return json.map((e) => _sanitizeJson(e)).toList(); + } + return json; + }"""; + + return result; +} + String createJsonSingleton(String classNameTrim, List generics) { if (generics.length == 0) // return ""; - var objects = generics.map((e) => "__Object").join(", "); + var objects = generics.map((e) => "Object").join(", "); var result = """ class ${classNameTrim}_Generics_Sing { - __Map<__List<__String>, $classNameTrim<${objects}> Function(__Map<__String, dynamic>)> fns = {}; + Map, $classNameTrim<${objects}> Function(Map)> fns = {}; factory ${classNameTrim}_Generics_Sing() => _singleton; static final ${classNameTrim}_Generics_Sing _singleton = ${classNameTrim}_Generics_Sing._internal(); ${classNameTrim}_Generics_Sing._internal() {} -} +} """; return result; } + +String commentEveryLine(String multilineString) { + return multilineString.split('\n').map((line) => '//' + line).join('\n'); +} + +String generateCompareExtension( + bool isAbstract, + String className, + String classNameTrim, + List allFields, + List knownInterfaces, // Add these parameters + List knownClasses, // Add these parameters + bool generateCompareTo, +) { + var sb = StringBuffer(); + String enumClassName = "${classNameTrim}\$"; + sb.writeln(); + sb.writeln("extension \$${classNameTrim}CompareE on \$${classNameTrim} {"); + + // First version with String keys + sb.writeln(''' + Map compareTo$classNameTrim($classNameTrim other) { + final Map diff = {}; + + ${_generateCompareFieldsLogic(allFields, knownInterfaces, knownClasses, useEnumKeys: false)} + + return diff; + } + '''); + + // Second version with Enum keys + // sb.writeln(''' + // ${classNameTrim}Patch compareToEnum$classNameTrim($classNameTrim other) { + // final ${classNameTrim}Patch diff = {}; + + // ${_generateCompareFieldsLogic(allFields, knownInterfaces, knownClasses, useEnumKeys: true, enumClassName: enumClassName)} + + // return diff; + // } + // '''); + + sb.writeln("}"); + return sb.toString(); +} + +String _generateCompareFieldsLogic(List allFields, + List knownInterfaces, List knownClasses, + {required bool useEnumKeys, String? enumClassName}) { + return allFields + .map((field) { + final type = field.type ?? ''; + final name = field.name; + final isNullable = type.endsWith('?'); + final keyString = + useEnumKeys ? '$enumClassName.${field.name}' : "'${field.name}'"; + final methodName = useEnumKeys ? 'compareToEnum' : 'compareTo'; + final baseType = type.replaceAll('?', ''); // Remove nullable indicator + + // Skip functions + if (type.contains('Function')) { + return ''; + } + + // Handle different types + if (type.startsWith('List<') || type.startsWith('Set<')) { + return _generateCollectionComparison( + name, type, keyString, isNullable); + } + + if (type.startsWith('Map<')) { + return _generateMapComparison(name, keyString, isNullable); + } + + if (type.contains('DateTime')) { + return _generateDateTimeComparison(name, keyString, isNullable); + } + + // Check if type is a known interface or class + final isKnownType = + knownInterfaces.any((i) => i.interfaceName == baseType) || + knownClasses.contains(baseType); + + if (isKnownType) { + return _generateKnownTypeComparison( + name, baseType, keyString, methodName, isNullable); + } + + // Direct comparison for all other types + return _generateSimpleComparison(name, keyString); + }) + .where((s) => s.isNotEmpty) + .join('\n '); +} + +String _generateCollectionComparison( + String name, String type, String keyString, bool isNullable) { + if (isNullable) { + return ''' + if ($name != other.$name) { + if ($name != null && other.$name != null) { + if ($name!.length != other.$name!.length) { + diff[$keyString] = () => other.$name; + } else { + var hasDiff = false; + for (var i = 0; i < $name!.length; i++) { + if ($name![i] != other.$name![i]) { + hasDiff = true; + break; + } + } + if (hasDiff) { + diff[$keyString] = () => other.$name; + } + } + } else { + diff[$keyString] = () => other.$name; + } + }'''; + } + + return ''' + if ($name != other.$name) { + if ($name.length != other.$name.length) { + diff[$keyString] = () => other.$name; + } else { + var hasDiff = false; + for (var i = 0; i < $name.length; i++) { + if ($name[i] != other.$name[i]) { + hasDiff = true; + break; + } + } + if (hasDiff) { + diff[$keyString] = () => other.$name; + } + } + }'''; +} + +String _generateMapComparison(String name, String keyString, bool isNullable) { + if (isNullable) { + return ''' + if ($name != other.$name) { + if ($name != null && other.$name != null) { + if ($name!.length != other.$name!.length || + !$name!.keys.every((k) => other.$name!.containsKey(k) && $name![k] == other.$name![k])) { + diff[$keyString] = () => other.$name; + } + } else { + diff[$keyString] = () => other.$name; + } + }'''; + } + + return ''' + if ($name != other.$name) { + if ($name.length != other.$name.length || + !$name.keys.every((k) => other.$name.containsKey(k) && $name[k] == other.$name[k])) { + diff[$keyString] = () => other.$name; + } + }'''; +} + +String _generateDateTimeComparison( + String name, String keyString, bool isNullable) { + if (isNullable) { + return ''' + if ($name != other.$name) { + if ($name != null && other.$name != null) { + if (!$name!.isAtSameMomentAs(other.$name!)) { + diff[$keyString] = () => other.$name; + } + } else { + diff[$keyString] = () => other.$name; + } + }'''; + } + + return ''' + if ($name != other.$name) { + if (!$name.isAtSameMomentAs(other.$name)) { + diff[$keyString] = () => other.$name; + } + }'''; +} + +String _generateKnownTypeComparison(String name, String baseType, + String keyString, String methodName, bool isNullable) { + if (isNullable) { + return ''' + if ($name != other.$name) { + if ($name != null && other.$name != null) { + diff[$keyString] = () => $name!.$methodName${baseType.replaceAll(RegExp(r'[^a-zA-Z0-9]'), '')}(other.$name!); + } else { + diff[$keyString] = () => other.$name; + } + }'''; + } + + return ''' + if ($name != other.$name) { + diff[$keyString] = () => $name.$methodName${baseType.replaceAll(RegExp(r'[^a-zA-Z0-9]'), '')}(other.$name); + }'''; +} + +String _generateSimpleComparison(String name, String keyString) { + return ''' + if ($name != other.$name) { + diff[$keyString] = () => other.$name; + }'''; +} diff --git a/morphy/pubspec.lock b/morphy/pubspec.lock index 58ea64d..51c4c07 100644 --- a/morphy/pubspec.lock +++ b/morphy/pubspec.lock @@ -303,7 +303,7 @@ packages: path: "../morphy_annotation" relative: true source: path - version: "1.0.0" + version: "2.0.0" node_preamble: dependency: transitive description: @@ -321,13 +321,13 @@ packages: source: hosted version: "2.1.0" path: - dependency: transitive + dependency: "direct main" description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.1" pool: dependency: transitive description: @@ -553,4 +553,4 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.1.3 <4.0.0" + dart: ">=3.4.0 <4.0.0" diff --git a/morphy/pubspec.yaml b/morphy/pubspec.yaml index 16ba19d..38ab5d5 100644 --- a/morphy/pubspec.yaml +++ b/morphy/pubspec.yaml @@ -1,23 +1,28 @@ name: morphy description: Provides a clean class definition with extra functionality including; copy with, json serializable, tostring, equals that supports inheritance and polymorphism -version: 1.2.0 +version: 2.0.0 homepage: https://github.com/atreeon/morphy environment: sdk: ">=3.1.3 <4.0.0" -#dependency_overrides: -# morphy_annotation: -# path: ../morphy_annotation - +# dependency_overrides: +# morphy_annotation: +# git: +# url: https://github.com/arrrrny/morphy.git +# path: morphy_annotation +# ref: git-ref +publish_to: none dependencies: - morphy_annotation: ^1.1.0 - analyzer: '>=6.0.0 <=7.0.0' + morphy_annotation: + path: ../morphy_annotation + analyzer: ">=6.0.0 <=7.0.0" build: ^2.1.0 source_gen: ^1.1.1 dartx: ^1.2.0 quiver: ^3.2.1 + path: ^1.8.1 dev_dependencies: build_runner: ^2.4.6 - test: ^1.24.8 \ No newline at end of file + test: ^1.24.8 diff --git a/morphy/test/helpers_test.dart b/morphy/test/helpers_test.dart index b11633d..969ae37 100644 --- a/morphy/test/helpers_test.dart +++ b/morphy/test/helpers_test.dart @@ -5,7 +5,8 @@ import 'package:morphy/src/common/formatCodeStringForComparison.dart'; import 'package:morphy/src/helpers.dart'; import 'package:test/test.dart'; -var expectS = (String a, String b) => expect(formatCodeStringForComparison(a), formatCodeStringForComparison(b)); +var expectS = (String a, String b) => + expect(formatCodeStringForComparison(a), formatCodeStringForComparison(b)); void main() { group("getClassComment", () { @@ -42,9 +43,11 @@ void main() { test("3x with all comments", () { var interfaces = [ - InterfaceWithComment("\$A", ["int", "String"], ["T1", "T2"], [], comment: "///blah1"), + InterfaceWithComment("\$A", ["int", "String"], ["T1", "T2"], [], + comment: "///blah1"), InterfaceWithComment("\$A", ["int", "String"], ["T1", "T2"], []), - InterfaceWithComment("\$A", ["int", "String"], ["T1", "T2"], [], comment: "///blah2"), + InterfaceWithComment("\$A", ["int", "String"], ["T1", "T2"], [], + comment: "///blah2"), ]; var result = getClassComment(interfaces, "///blah"); @@ -171,19 +174,22 @@ void main() { group("getClassDefinition", () { test("1b", () { - var result = getClassDefinition(isAbstract: false, nonSealed: false, className: "\$Pet"); + var result = getClassDefinition( + isAbstract: false, nonSealed: false, className: "\$Pet"); expectS(result, "class Pet"); }); test("2b", () { - var result = getClassDefinition(isAbstract: true, nonSealed: false, className: "\$\$Pet"); + var result = getClassDefinition( + isAbstract: true, nonSealed: false, className: "\$\$Pet"); expectS(result, "sealed class Pet"); }); test("3b", () { - var result = getClassDefinition(isAbstract: true, nonSealed: true, className: "\$\$Pet"); + var result = getClassDefinition( + isAbstract: true, nonSealed: true, className: "\$\$Pet"); expectS(result, "abstract class Pet"); }); @@ -209,7 +215,8 @@ void main() { group("getExtendsGenerics", () { test("1d", () { - var result = getExtendsGenerics([NameTypeClassComment("T", "\$\$C", null)]); + var result = + getExtendsGenerics([NameTypeClassComment("T", "\$\$C", null)]); expectS(result, ""); }); @@ -310,21 +317,23 @@ void main() { expectS(result.toString(), "final List schedules;"); }); - test("7f private properties (get turned into public getters & private setters)", () { + test( + "7f private properties (get turned into public getters & private setters)", + () { var result = getProperties([ NameTypeClassComment("_age", "int", null), NameTypeClassComment("name", "String", null, comment: "///blah"), ]); - expectS(result.toString(), "final int _age;\n///blah\nfinal String name;"); + expectS( + result.toString(), "final int _age;\n///blah\nfinal String name;"); }); }); group("getToString", () { test("1g", () { var result = getToString([], "MyClass"); - - expectS(result.toString(), """__String toString() => "(MyClass-)"""); + expectS(result.toString(), """String toString() => "(MyClass-)"""); }); test("2g", () { @@ -334,7 +343,8 @@ void main() { NameTypeClassComment("c", "String", null), ], "MyClass"); - expectS(result.toString(), """__String toString() => "(MyClass-a:\${a.toString()}|b:\${b.toString()}|c:\${c.toString()})";"""); + expectS(result.toString(), + """String toString() => "(MyClass-a:\${a.toString()}|b:\${b.toString()}|c:\${c.toString()})";"""); }); }); @@ -352,17 +362,16 @@ void main() { NameTypeClassComment("c", "String", null), ]); - expectS( - result.toString(), // - """__int get hashCode => __hashObjects([a.hashCode, b.hashCode, c.hashCode]);"""); + expectS(result.toString(), + """int get hashCode => hashObjects([a.hashCode, b.hashCode, c.hashCode]);"""); }); }); group("getEquals", () { test("1i", () { var result = getEquals([], "A"); - - var expected = """__bool operator ==(__Object other) => __identical(this, other) || other is A && runtimeType == other.runtimeType + var expected = + """bool operator ==(Object other) => identical(this, other) || other is A && runtimeType == other.runtimeType ;"""; expectS(result, expected); @@ -374,8 +383,8 @@ void main() { NameTypeClassComment("b", "String", null), NameTypeClassComment("c", "String", null), ], "C"); - - var expected = """__bool operator ==(__Object other) => __identical(this, other) || other is C && runtimeType == other.runtimeType && + var expected = + """bool operator ==(Object other) => identical(this, other) || other is C && runtimeType == other.runtimeType && a == other.a && b == other.b && c == other.c;"""; expectS(result, expected); @@ -404,7 +413,8 @@ a == other.a && b == other.b && c == other.c;"""; NameTypeClassComment("name", "String", null), ]); - expectS(result.toString(), "///blah blah\nint get age;\nString get name;"); + expectS( + result.toString(), "///blah blah\nint get age;\nString get name;"); }); }); @@ -439,7 +449,8 @@ a == other.a && b == other.b && c == other.c;"""; ], ); - expectS(result.toString(), "required this.age,\nrequired this.listOfStrings,"); + expectS(result.toString(), + "required this.age,\nrequired this.listOfStrings,"); }); test("5k private properties not null", () { @@ -467,7 +478,7 @@ a == other.a && b == other.b && c == other.c;"""; group("get intializer list", () { test("1l with a private", () { - var result = getInitialiser( + var result = getInitializer( [ NameTypeClassComment("_age", "int?", null), NameTypeClassComment("name", "String?", null), @@ -478,7 +489,7 @@ a == other.a && b == other.b && c == other.c;"""; }); test("2l without a private", () { - var result = getInitialiser( + var result = getInitializer( [ NameTypeClassComment("age", "int?", null), NameTypeClassComment("name", "String?", null), @@ -509,8 +520,11 @@ a == other.a && b == other.b && c == other.c;"""; expectS(result, "Word"); }); - test("4m if a Function data type and we have a morphy class then don't remove", () { - var result = removeDollarsFromPropertyType("bool Function(int blah, \$X blim)"); + test( + "4m if a Function data type and we have a morphy class then don't remove", + () { + var result = + removeDollarsFromPropertyType("bool Function(int blah, \$X blim)"); expectS(result, "bool Function(int blah, \$X blim)"); }); }); @@ -537,7 +551,8 @@ a == other.a && b == other.b && c == other.c;"""; ], "\$MyClass", ); - expectS(result, "enum MyClass\$ {age,name}"); + expectS(result, + "typedef MyClassPatch = Map;\nenum MyClass\$ {age,name}"); }); test("2o no fields", () { @@ -563,7 +578,7 @@ a == other.a && b == other.b && c == other.c;"""; isClassAbstract: true, interfaceGenerics: [], ); - expectS(result, """A copyWith_A({ + expectS(result, """A copyWithA({ String Function()? a, });"""); }); @@ -582,7 +597,7 @@ String Function()? a, isClassAbstract: true, interfaceGenerics: [], ); - expectS(result, """B copyWith_A({ + expectS(result, """A copyWithA({ int Function()? a, });"""); }); @@ -600,13 +615,12 @@ int Function()? a, isClassAbstract: false, interfaceGenerics: [], ); - expectS(result, """A copyWith_A({ + expectS(result, """A copyWithA({ String Function()? a, }) { return A._( a: a == null ? this.a as String : a() as String, -); -}"""); +) as A;}"""); }); test("4p ba (see ex29_manual)", () { @@ -623,14 +637,13 @@ a: a == null ? this.a as String : a() as String, isClassAbstract: false, interfaceGenerics: [], ); - expectS(result, """B copyWith_A({ + expectS(result, """A copyWithA({ String Function()? a, }) { return B._( a: a == null ? this.a as String : a() as String, b: (this as B).b, -); -}"""); +) as A;}"""); }); test("5p bb (see ex29_manual)", () { @@ -648,15 +661,14 @@ b: (this as B).b, isClassAbstract: false, interfaceGenerics: [], ); - expectS(result, """B copyWith_B({ + expectS(result, """B copyWithB({ String Function()? a, T1 Function()? b, }) { return B._( a: a == null ? this.a as String : a() as String, b: b == null ? this.b as T1 : b() as T1, -); -}"""); +) as B;}"""); }); test("6p ca (see ex29_manual)", () { @@ -674,15 +686,14 @@ b: b == null ? this.b as T1 : b() as T1, isClassAbstract: false, interfaceGenerics: [], ); - expectS(result, """C copyWith_A({ + expectS(result, """A copyWithA({ String Function()? a, }) { return C._( a: a == null ? this.a as String : a() as String, b: (this as C).b, c: (this as C).c, -); -}"""); +) as A;}"""); }); test("7p cb (see ex29_manual)", () { @@ -701,7 +712,7 @@ c: (this as C).c, isClassAbstract: false, interfaceGenerics: [], ); - expectS(result, """C copyWith_B({ + expectS(result, """B copyWithB({ String Function()? a, T1 Function()? b, }) { @@ -709,8 +720,7 @@ return C._( a: a == null ? this.a as String : a() as String, b: b == null ? this.b as T1 : b() as T1, c: (this as C).c, -); -}"""); +) as B;}"""); }); test("8p cc (see ex29_manual)", () { @@ -730,7 +740,7 @@ c: (this as C).c, isClassAbstract: false, interfaceGenerics: [], ); - expectS(result, """C copyWith_C({ + expectS(result, """C copyWithC({ String Function()? a, T1 Function()? b, bool Function()? c, @@ -739,8 +749,7 @@ return C._( a: a == null ? this.a as String : a() as String, b: b == null ? this.b as T1 : b() as T1, c: c == null ? this.c as bool : c() as bool, -); -}"""); +) as C;}"""); }); test("9p da (see ex29_manual) class with no fields", () { @@ -757,14 +766,13 @@ c: c == null ? this.c as bool : c() as bool, isClassAbstract: false, interfaceGenerics: [], ); - expectS(result, """D copyWith_A({ + expectS(result, """A copyWithA({ String Function()? a, }) { return D._( a: a == null ? this.a as String : a() as String, b: (this as D).b, -); -}"""); +) as A;}"""); }); test("10p db (see ex29_manual) class with no fields", () { @@ -782,15 +790,14 @@ b: (this as D).b, isClassAbstract: false, interfaceGenerics: [], ); - expectS(result, """D copyWith_B({ + expectS(result, """B copyWithB({ String Function()? a, T1 Function()? b, }) { return D._( a: a == null ? this.a as String : a() as String, b: b == null ? this.b as T1 : b() as T1, -); -}"""); +) as B;}"""); }); test("11p dd (see ex29_manual) class with no fields", () { @@ -808,15 +815,14 @@ b: b == null ? this.b as T1 : b() as T1, isClassAbstract: false, interfaceGenerics: [], ); - expectS(result, """D copyWith_D({ + expectS(result, """D copyWithD({ String Function()? a, T1 Function()? b, }) { return D._( a: a == null ? this.a as String : a() as String, b: b == null ? this.b as T1 : b() as T1, -); -}"""); +) as D;}"""); }); test("12p x (see ex29_manual) interface with no fields", () { @@ -828,7 +834,7 @@ b: b == null ? this.b as T1 : b() as T1, isClassAbstract: true, interfaceGenerics: [], ); - expectS(result, """X copyWith_X( + expectS(result, """X copyWithX( );"""); }); @@ -843,12 +849,11 @@ b: b == null ? this.b as T1 : b() as T1, isClassAbstract: false, interfaceGenerics: [], ); - expectS(result, """Y copyWith_X( + expectS(result, """X copyWithX( ) { return Y._( a: (this as Y).a, -); -}"""); +) as X;}"""); }); test("14p yy (see ex29_manual) interface with no fields", () { @@ -864,13 +869,12 @@ a: (this as Y).a, isClassAbstract: false, interfaceGenerics: [], ); - expectS(result, """Y copyWith_Y({ + expectS(result, """Y copyWithY({ String Function()? a, }) { return Y._( a: a == null ? this.a as String : a() as String, -); -}"""); +) as Y;}"""); }); test("15p aa (see ex7_manual) where subtypes are used", () { @@ -886,13 +890,12 @@ a: a == null ? this.a as String : a() as String, isClassAbstract: false, interfaceGenerics: [], ); - expectS(result, """A copyWith_A({ + expectS(result, """A copyWithA({ Person Function()? a, }) { return A._( a: a == null ? this.a as Person : a() as Person, -); -}"""); +) as A;}"""); }); test("16p ba (see ex7_manual) where subtypes are used", () { @@ -908,13 +911,12 @@ a: a == null ? this.a as Person : a() as Person, isClassAbstract: false, interfaceGenerics: [], ); - expectS(result, """B copyWith_A({ + expectS(result, """A copyWithA({ Person Function()? a, }) { return B._( a: a == null ? this.a as Employee : a() as Employee, -); -}"""); +) as A;}"""); }); test("17p bb (see ex7_manual) where subtypes are used", () { @@ -930,13 +932,12 @@ a: a == null ? this.a as Employee : a() as Employee, isClassAbstract: false, interfaceGenerics: [], ); - expectS(result, """B copyWith_B({ + expectS(result, """B copyWithB({ Employee Function()? a, }) { return B._( a: a == null ? this.a as Employee : a() as Employee, -); -}"""); +) as B;}"""); }); test("18p ca (see ex7_manual) where subtypes are used", () { @@ -952,13 +953,12 @@ a: a == null ? this.a as Employee : a() as Employee, isClassAbstract: false, interfaceGenerics: [], ); - expectS(result, """C copyWith_A({ + expectS(result, """A copyWithA({ Person Function()? a, }) { return C._( a: a == null ? this.a as Manager : a() as Manager, -); -}"""); +) as A;}"""); }); test("19p cb (see ex7_manual) where subtypes are used", () { @@ -974,13 +974,12 @@ a: a == null ? this.a as Manager : a() as Manager, isClassAbstract: false, interfaceGenerics: [], ); - expectS(result, """C copyWith_B({ + expectS(result, """B copyWithB({ Employee Function()? a, }) { return C._( a: a == null ? this.a as Manager : a() as Manager, -); -}"""); +) as B;}"""); }); test("20p cc (see ex7_manual) where subtypes are used", () { @@ -996,13 +995,12 @@ a: a == null ? this.a as Manager : a() as Manager, isClassAbstract: false, interfaceGenerics: [], ); - expectS(result, """C copyWith_C({ + expectS(result, """C copyWithC({ Manager Function()? a, }) { return C._( a: a == null ? this.a as Manager : a() as Manager, -); -}"""); +) as C;}"""); }); test("21p ba (see ex11_manual) where subtypes are used", () { @@ -1021,7 +1019,7 @@ a: a == null ? this.a as Manager : a() as Manager, className: "B", isClassAbstract: false, ); - expectS(result, """B copyWith_A({ + expectS(result, """A copyWithA({ T1 Function()? x, T2 Function()? y, }) { @@ -1029,8 +1027,7 @@ return B._( x: x == null ? this.x as int : x() as int, y: y == null ? this.y as String : y() as String, z: (this as B).z, -); -}"""); +) as A;}"""); }); test("22p bb (see ex11_manual) where subtypes are used", () { @@ -1050,7 +1047,7 @@ z: (this as B).z, className: "B", isClassAbstract: false, ); - expectS(result, """B copyWith_B({ + expectS(result, """B copyWithB({ int Function()? x, String Function()? y, String Function()? z, @@ -1059,8 +1056,7 @@ return B._( x: x == null ? this.x as int : x() as int, y: y == null ? this.y as String : y() as String, z: z == null ? this.z as String : z() as String, -); -}"""); +) as B;}"""); }); test("23p a (see ex2_manual) where generics are used", () { @@ -1076,7 +1072,7 @@ z: z == null ? this.z as String : z() as String, className: "A", isClassAbstract: true, ); - expectS(result, """A copyWith_A({ + expectS(result, """A copyWithA({ T Function()? x, });"""); }); @@ -1096,14 +1092,13 @@ T Function()? x, className: "B", isClassAbstract: false, ); - expectS(result, """B copyWith_A({ + expectS(result, """A copyWithA({ T Function()? x, }) { return B._( x: x == null ? this.x as int : x() as int, y: (this as B).y, -); -}"""); +) as A;}"""); }); test("25p a (see ex21) no default constructor", () { @@ -1119,13 +1114,12 @@ y: (this as B).y, className: "A", isClassAbstract: false, ); - expectS(result, """A copyWith_A({ + expectS(result, """A copyWithA({ String Function()? a, }) { return A._( a: a == null ? this.a as String : a() as String, -); -}"""); +) as A;}"""); }); test("26p function to leave in dollar", () { @@ -1141,13 +1135,12 @@ a: a == null ? this.a as String : a() as String, className: "X", isClassAbstract: false, ); - expectS(result, """X copyWith_X({ + expectS(result, """X copyWithX({ bool Function(\$X) Function()? fn, }) { return X._( fn: fn == null ? this.fn as bool Function(\$X) : fn() as bool Function(\$X), -); -}"""); +) as X;}"""); }); test("27p subtype from a supertype", () { @@ -1165,15 +1158,14 @@ fn: fn == null ? this.fn as bool Function(\$X) : fn() as bool Function(\$X), isClassAbstract: false, isExplicitSubType: true, ); - expectS(result, """B changeTo_B({ + expectS(result, """B changeToB({ required String y, String Function()? x, }) { -return B._( +return B( y: y as String, x: x == null ? this.x as String : x() as String, -); -}"""); +) as B;}"""); }); test("28p subtype from a supertype", () { @@ -1192,17 +1184,16 @@ x: x == null ? this.x as String : x() as String, isClassAbstract: false, isExplicitSubType: true, ); - expectS(result, """B changeTo_B({ + expectS(result, """B changeToB({ required String y, required Z z, String Function()? x, }) { -return B._( +return B( y: y as String, z: z as Z, x: x == null ? this.x as String : x() as String, -); -}"""); +) as B;}"""); }); test("29p sub to sub sibling with abstract parent", () { @@ -1221,17 +1212,16 @@ x: x == null ? this.x as String : x() as String, isClassAbstract: false, isExplicitSubType: true, ); - expectS(result, """B changeTo_B({ + expectS(result, """B changeToB({ required String y, required Z z, String Function()? x, }) { -return B._( +return B( y: y as String, z: z as Z, x: x == null ? this.x as String : x() as String, -); -}"""); +) as B;}"""); }); test("30p function to leave in dollar", () { @@ -1251,7 +1241,7 @@ x: x == null ? this.x as String : x() as String, className: "\$B", isClassAbstract: false, ); - expectS(result, """B copyWith_B({ + expectS(result, """B copyWithB({ String Function()? x, List Function()? cs, Z Function()? z, @@ -1260,8 +1250,7 @@ return B._( x: x == null ? this.x as String : x() as String, cs: cs == null ? this.cs as List : cs() as List, z: z == null ? this.z as Z : z() as Z, -); -}"""); +) as B;}"""); }); test("31p FROM ABSTRACT SUPERCLASS TO SUB CLASS", () { @@ -1279,15 +1268,14 @@ z: z == null ? this.z as Z : z() as Z, isClassAbstract: true, isExplicitSubType: true, ); - expectS(result, """B changeTo_B({ + expectS(result, """B changeToB({ required String y, String Function()? x, }) { -return B._( +return B( y: y as String, x: x == null ? this.x as String : x() as String, -); -}"""); +) as B;}"""); }); test("32p private constructor", () { @@ -1306,17 +1294,16 @@ x: x == null ? this.x as String : x() as String, isClassAbstract: false, isExplicitSubType: true, ); - expectS(result, """B changeTo_B({ + expectS(result, """B changeToB({ required String y, required Z z, String Function()? x, }) { -return B._( +return B( y: y as String, z: z as Z, x: x == null ? this.x as String : x() as String, -); -}"""); +) as B;}"""); }); test("33p private property abstract class", () { @@ -1332,7 +1319,7 @@ x: x == null ? this.x as String : x() as String, isClassAbstract: true, interfaceGenerics: [], ); - expectS(result, """A copyWith_A({ + expectS(result, """A copyWithA({ String Function()? a, });"""); }); @@ -1353,7 +1340,7 @@ String Function()? a, isClassAbstract: false, interfaceGenerics: [], ); - expectS(result, """C copyWith_B({ + expectS(result, """B copyWithB({ String Function()? a, T1 Function()? b, }) { @@ -1361,8 +1348,7 @@ return C._( a: a == null ? this._a as String : a() as String, b: b == null ? this.b as T1 : b() as T1, c: (this as C).c, -); -}"""); +) as B;}"""); }); }); @@ -1372,8 +1358,11 @@ c: (this as C).c, expectS(result, "Word"); }); - test("2q if a Function data type and we have a morphy class then don't remove", () { - var result = getDataTypeWithoutDollars("bool Function(int blah, \$X blim)"); + test( + "2q if a Function data type and we have a morphy class then don't remove", + () { + var result = + getDataTypeWithoutDollars("bool Function(int blah, \$X blim)"); expectS(result, "bool Function(int blah, \$X blim)"); }); }); @@ -1381,23 +1370,20 @@ c: (this as C).c, group("generateFromJsonHeader", () { test("1r ", () { var result = generateFromJsonHeader("\$Pet"); - expectS(result, "factory Pet.fromJson(__Map<__String, dynamic> json) {"); + expectS(result, "factory Pet.fromJson(Map json) {"); }); }); group("generateFromJsonBody", () { test("1s no explicit", () { var result = generateFromJsonBody("\$Pet", [], []); - - var expected = """ - if (json['_className_'] == "Pet") { - - return _\$PetFromJson(json, ); - } else { - throw UnsupportedError("The _className_ '\${json['_className_']}' is not supported by the Pet.fromJson constructor."); - } - }"""; - + var expected = """if (json['_className_'] == null) { + return _\$PetFromJson(json); + } + if (json['_className_'] == "Pet") { + return _\$PetFromJson(json); + } throw UnsupportedError("The _className_ '\${json['_className_']}' is not supported by the Pet.fromJson constructor."); + }"""; expectS(result, expected); }); @@ -1406,20 +1392,18 @@ c: (this as C).c, "\$Person", [], [ - Interface("\$Manager", [], [], [NameType("hairLength", "int")]), + Interface("\$Manager", [], [], [NameType("hairLength", "int")]) ], ); - - var expected = """ - if (json['_className_'] == "Manager") { - return _\$ManagerFromJson(json, ); - } else if (json['_className_'] == "Person") { - return _\$PersonFromJson(json, ); - } else { - throw UnsupportedError("The _className_ '\${json['_className_']}' is not supported by the Person.fromJson constructor."); - } - }"""; - + var expected = """if (json['_className_'] == null) { + return _\$PersonFromJson(json); + } + if (json['_className_'] == "Manager") { + return Manager.fromJson(json); + } else if (json['_className_'] == "Person") { + return _\$PersonFromJson(json); + } throw UnsupportedError("The _className_ '\${json['_className_']}' is not supported by the Person.fromJson constructor."); + }"""; expectS(result, expected); }); @@ -1434,18 +1418,18 @@ c: (this as C).c, [], ); - var expected = """ - if (json['_className_'] == "B") { - var fn_fromJson = getFromJsonToGenericFn( - B_Generics_Sing().fns, - json, - ['_T_','_T2_','_T3_'], - ); - return fn_fromJson(json); - } else { - throw UnsupportedError("The _className_ '\${json['_className_']}' is not supported by the B.fromJson constructor."); - } -}"""; + var expected = """if (json['_className_'] == null) { + return _\$BFromJson(json); + } + if (json['_className_'] == "B") { + var fn_fromJson = getFromJsonToGenericFn( + B_Generics_Sing().fns, + json, + ['_T_','_T2_','_T3_'], + ); + return fn_fromJson(json); + } throw UnsupportedError("The _className_ '\${json['_className_']}' is not supported by the B.fromJson constructor."); + }"""; expectS(result, expected); }); @@ -1455,24 +1439,24 @@ c: (this as C).c, "A", [], [ - Interface("\$B", ["", "", ""], ["T", "T2", "T3"], []), + Interface("\$B", ["", "", ""], ["T", "T2", "T3"], []) ], ); - var expected = """ - if (json['_className_'] == "B") { - var fn_fromJson = getFromJsonToGenericFn( - B_Generics_Sing().fns, - json, - ['_T_','_T2_','_T3_'], - ); - return fn_fromJson(json); - } else if (json[\'_className_\'] == "A") { - return _\$AFromJson(json, ); - } else { - throw UnsupportedError("The _className_ '\${json['_className_']}' is not supported by the A.fromJson constructor."); - } -}"""; + var expected = """if (json['_className_'] == null) { + return _\$AFromJson(json); + } + if (json['_className_'] == "B") { + var fn_fromJson = getFromJsonToGenericFn( + B_Generics_Sing().fns, + json, + ['_T_','_T2_','_T3_'], + ); + return fn_fromJson(json); + } else if (json['_className_'] == "A") { + return _\$AFromJson(json); + } throw UnsupportedError("The _className_ '\${json['_className_']}' is not supported by the A.fromJson constructor."); + }"""; expectS(result, expected); }); @@ -1482,52 +1466,44 @@ c: (this as C).c, "\$\$A", [], [ - Interface("\$B", ["", "", ""], ["T", "T2", "T3"], []), + Interface("\$B", ["", "", ""], ["T", "T2", "T3"], []) ], ); - var expected = """ - if (json['_className_'] == "B") { - var fn_fromJson = getFromJsonToGenericFn( - B_Generics_Sing().fns, - json, - ['_T_','_T2_','_T3_'], - ); - return fn_fromJson(json); - } else { - throw UnsupportedError("The _className_ '\${json['_className_']}' is not supported by the A.fromJson constructor."); - } -}"""; + var expected = """if (json['_className_'] == null) { + return _\$AFromJson(json); + } + if (json['_className_'] == "B") { + var fn_fromJson = getFromJsonToGenericFn( + B_Generics_Sing().fns, + json, + ['_T_','_T2_','_T3_'], + ); + return fn_fromJson(json); + } throw UnsupportedError("The _className_ '\${json['_className_']}' is not supported by the A.fromJson constructor."); + }"""; expectS(result, expected); }); }); - group("generateToJson", () { //add this based on the generics test("1u", () { var result = generateToJson("\$Pet", []); - - var expected = """// ignore: unused_field\n - __Map<__Type, __Object? Function(__Never)> _fns = {}; - - __Map<__String, dynamic> toJson_2([__Map<__Type, __Object? Function(__Never)>? fns]){ - this._fns = fns ?? {}; - return toJson(); - } - - __Map<__String, dynamic> toJson() { - - final __Map<__String, dynamic> data = _\$PetToJson(this, -); - // Adding custom key-value pair - data['_className_'] = 'Pet'; - - - return data; - }"""; - + var expected = """// ignore: unused_field + Map _fns = {}; + + Map toJsonCustom([Map? fns]){ + _fns = fns ?? {}; + return toJson(); + } + + Map toJson() { + final Map data = _\$PetToJson(this); + data['_className_'] = 'Pet'; + return data; + }"""; expectS(result, expected); }); @@ -1540,32 +1516,29 @@ c: (this as C).c, NameType("T3", null), ], ); - - var expected = """// ignore: unused_field\n - __Map<__Type, __Object? Function(__Never)> _fns = {}; - - __Map<__String, dynamic> toJson_2([__Map<__Type, __Object? Function(__Never)>? fns]){ - this._fns = fns ?? {}; - return toJson(); - } - - __Map<__String, dynamic> toJson() { - var fn_T = getGenericToJsonFn(_fns, T); - var fn_T2 = getGenericToJsonFn(_fns, T2); - var fn_T3 = getGenericToJsonFn(_fns, T3); - final __Map<__String, dynamic> data = _\$PetToJson(this, - fn_T as __Object? Function(T), - fn_T2 as __Object? Function(T2), - fn_T3 as __Object? Function(T3)); - // Adding custom key-value pair - data['_className_'] = 'Pet'; - data['_T_'] = T.toString(); - data['_T2_'] = T2.toString(); - data['_T3_'] = T3.toString(); - - return data; - }"""; - + var expected = """// ignore: unused_field + Map _fns = {}; + + Map toJsonCustom([Map? fns]){ + _fns = fns ?? {}; + return toJson(); + } + + Map toJson() { + var fn_T = getGenericToJsonFn(_fns, T); + var fn_T2 = getGenericToJsonFn(_fns, T2); + var fn_T3 = getGenericToJsonFn(_fns, T3); + final Map data = _\$PetToJson(this, + fn_T as Object? Function(T), + fn_T2 as Object? Function(T2), + fn_T3 as Object? Function(T3) + ); + data['_className_'] = 'Pet'; + data['_T_'] = T.toString(); + data['_T2_'] = T2.toString(); + data['_T3_'] = T3.toString(); + return data; + }"""; expectS(result, expected); }); @@ -1573,7 +1546,7 @@ c: (this as C).c, var result = generateToJson("\$\$Pet", []); var expected = """ - __Map<__String, dynamic> toJson_2([__Map<__Type, __Object? Function(__Never)>? fns]); + Map toJsonCustom([Map? fns]); """; expectS(result, expected); @@ -1591,7 +1564,7 @@ c: (this as C).c, var expected = """ class B_Generics_Sing { - __Map<__List<__String>, B<__Object> Function(__Map<__String, dynamic>)> fns = {}; + Map, B Function(Map)> fns = {}; factory B_Generics_Sing() => _singleton; static final B_Generics_Sing _singleton = B_Generics_Sing._internal(); @@ -1615,7 +1588,7 @@ class B_Generics_Sing { var expected = """ class C_Generics_Sing { - __Map<__List<__String>, C<__Object, __Object, __Object> Function(__Map<__String, dynamic>)> fns = {}; + Map, C Function(Map)> fns = {}; factory C_Generics_Sing() => _singleton; static final C_Generics_Sing _singleton = C_Generics_Sing._internal(); @@ -1641,25 +1614,89 @@ class C_Generics_Sing { group("createJsonHeader", () { test("1w non abstract, no generics, private constructor", () { - var result = createJsonHeader("\$Pet", [], true, true); - var expected = "@JsonSerializable(explicitToJson: true, constructor: 'forJsonDoNotUse')"; + var result = createJsonHeader("\$Pet", [], true, true, true); + var expected = + "@JsonSerializable(explicitToJson: true, constructor: 'forJsonDoNotUse')"; expectS(result, expected); }); test("2w abstract", () { - var result = createJsonHeader("\$\$Pet", [], true, true); + var result = createJsonHeader("\$\$Pet", [], true, true, true); var expected = ""; expectS(result, expected); }); test("3w non abstract, generics, no private constructor", () { - var result = createJsonHeader("\$Pet", [NameTypeClass("name", "type", "className")], false, false); - var expected = "@JsonSerializable(explicitToJson: false, genericArgumentFactories: true, )"; + var result = createJsonHeader("\$Pet", + [NameTypeClass("name", "type", "className")], false, false, false); + var expected = + "@JsonSerializable(explicitToJson: false, genericArgumentFactories: true, )"; + expectS(result, expected); + }); + }); + group("generateToJsonLean", () { + test("1z non-abstract class", () { + var result = generateToJsonLean("\$Pet"); + var expected = """ + + Map toJsonLean() { + final Map data = _\$PetToJson(this,); + return _sanitizeJson(data); + } + + dynamic _sanitizeJson(dynamic json) { + if (json is Map) { + json.remove('_className_'); + return json..forEach((key, value) { + json[key] = _sanitizeJson(value); + }); + } else if (json is List) { + return json.map((e) => _sanitizeJson(e)).toList(); + } + return json; + }"""; expectS(result, expected); }); + + test("2z abstract class", () { + var result = generateToJsonLean("\$\$Pet"); + expectS(result, ""); + }); + + // Modify verification tests to match actual implementation + test("3z verify sanitization", () { + var result = generateToJsonLean("\$Pet"); + + // Verify key components are present + expect(result.contains("_sanitizeJson"), true); + expect(result.contains("json.remove('_className_')"), true); + expect(result.contains("json is Map"), true); + expect(result.contains("json is List"), true); + }); + + test("4z verify different data types", () { + var result = generateToJsonLean("\$Pet"); + + // Verify handling of different types + expect(result.contains("return json;"), true); + expect(result.contains("json is Map"), true); + expect(result.contains("json is List"), true); + expect(result.contains("_sanitizeJson(value)"), true); + expect(result.contains("_sanitizeJson(e)"), true); + }); + + test("5z verify method structure", () { + var result = generateToJsonLean("\$Pet"); + + // Verify method signature and structure + expect(result.contains("Map toJsonLean()"), true); + expect(result.contains("_\$PetToJson(this,)"), + true); // Note the trailing comma + expect(result.contains("return _sanitizeJson(data)"), true); + }); }); // group("getCopyWithSignature", () { @@ -1671,7 +1708,7 @@ class C_Generics_Sing { // ], // "A", // ); -// expectS(result, """A copyWith_A({ +// expectS(result, """A copyWithA({ //required int a, //required String? b, //}) {"""); diff --git a/morphy_annotation/lib/src/List_E.dart b/morphy_annotation/lib/src/List_E.dart index b592910..684c623 100644 --- a/morphy_annotation/lib/src/List_E.dart +++ b/morphy_annotation/lib/src/List_E.dart @@ -44,7 +44,7 @@ extension List_E on List { } } -extension List__E on List { +extension ListE on List { bool equalOrderedD(List compareTo) { return _listDeepEqualsOrdered(this, compareTo); } diff --git a/morphy_annotation/lib/src/annotations.dart b/morphy_annotation/lib/src/annotations.dart index 94a18f2..52216c3 100644 --- a/morphy_annotation/lib/src/annotations.dart +++ b/morphy_annotation/lib/src/annotations.dart @@ -1,12 +1,22 @@ import 'package:collection/collection.dart'; /// {@macro MorphyX} -const morphy = Morphy(generateJson: false, explicitSubTypes: null); +const morphy = Morphy( + generateJson: false, + explicitSubTypes: null, + explicitToJson: true, + generateCompareTo: true, +); /// ### Morphy2 will be created before Morphy, sometimes the generator needs a related class to be built before another. /// --- /// {@macro MorphyX} -const morphy2 = Morphy2(generateJson: false, explicitSubTypes: null); +const morphy2 = Morphy2( + generateJson: false, + explicitSubTypes: null, + explicitToJson: true, + generateCompareTo: true, +); class Morphy implements MorphyX { /// if we want a copyWith (cwX) method for a subtype in this same class @@ -14,7 +24,7 @@ class Morphy implements MorphyX { final bool generateJson; final bool explicitToJson; - + final bool generateCompareTo; final bool hidePublicConstructor; ///if we specify the class as an abstract class we make it abstract and not sealed @@ -100,6 +110,7 @@ class Morphy implements MorphyX { this.explicitToJson = true, this.hidePublicConstructor = false, this.nonSealed = false, + this.generateCompareTo = true, }); } @@ -109,6 +120,7 @@ class Morphy2 implements MorphyX { final bool explicitToJson; final bool hidePublicConstructor; final bool nonSealed; + final bool generateCompareTo; const Morphy2({ this.explicitSubTypes = null, @@ -116,6 +128,7 @@ class Morphy2 implements MorphyX { this.explicitToJson = true, this.hidePublicConstructor = false, this.nonSealed = false, + this.generateCompareTo = true, }); } @@ -145,7 +158,9 @@ dynamic getFromJsonToGenericFn( ) { var types = genericType.map((e) => json[e]).toList(); - var fromJsonToGeneric_fn = fns.entries.firstWhereOrNull((entry) => ListEquality().equals(entry.key, types))?.value; + var fromJsonToGeneric_fn = fns.entries + .firstWhereOrNull((entry) => ListEquality().equals(entry.key, types)) + ?.value; if (fromJsonToGeneric_fn == null) // throw Exception("From JSON function not found"); return fromJsonToGeneric_fn; diff --git a/morphy_annotation/pubspec.lock b/morphy_annotation/pubspec.lock index d8bdb50..58eff44 100644 --- a/morphy_annotation/pubspec.lock +++ b/morphy_annotation/pubspec.lock @@ -41,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" matcher: dependency: transitive description: diff --git a/morphy_annotation/pubspec.yaml b/morphy_annotation/pubspec.yaml index cf0b44f..b46a418 100644 --- a/morphy_annotation/pubspec.yaml +++ b/morphy_annotation/pubspec.yaml @@ -1,6 +1,6 @@ name: morphy_annotation description: annotation for morphy which provides a clean class definition with extra functionality including; copy with, json serializable, tostring, equals that supports inheritance and polymorphism -version: 1.2.0 +version: 2.0.0 homepage: https://github.com/atreeon/morphy environment: @@ -11,6 +11,3 @@ dependencies: dartx: ^1.2.0 json_annotation: ^4.7.0 quiver: ^3.0.1 - - -