diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index b83ca908..c89cfa98 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -55,6 +55,18 @@ cd packages/luthor_generator/example dart run build_runner build -d ``` +### Testing luthor_generator +The `luthor_generator` package has tests that use generated code from test fixtures: +```bash +cd packages/luthor_generator +# Generate code for test fixtures +dart run build_runner build --delete-conflicting-outputs +# Run tests +dart test +``` + +Tests are automatically run in CI which includes building the test fixtures. + ### Documentation The documentation site is located in `./docs` and is built with Astro. @@ -254,7 +266,7 @@ For each annotated class, generate: ### Supporting Code Generation 1. Create annotation in `lib/src/annotations/validators/` 2. Update generator to handle the new annotation -3. Add tests for generated code +3. Add tests for generated code in `test/` using fixtures 4. Update example to demonstrate usage ## Resources diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index a3260a2e..1d8f384e 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -11,5 +11,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: ./.github/actions/setup-luthor + - name: Setup project + uses: ./.github/actions/setup-luthor + - name: Build generator test fixtures + run: dpk run build - run: dpk run test diff --git a/dpk.yaml b/dpk.yaml index 5e5a4ada..d826719b 100644 --- a/dpk.yaml +++ b/dpk.yaml @@ -22,10 +22,16 @@ scripts: runInPackages: - packages/* + build: + command: dart run build_runner build --force-aot + runInPackages: + - packages/luthor_generator + test: command: dart test runInPackages: - packages/luthor + - packages/luthor_generator publish:luthor: command: dart pub publish -f diff --git a/packages/luthor_generator/build.yaml b/packages/luthor_generator/build.yaml index 7f49153d..1e663018 100644 --- a/packages/luthor_generator/build.yaml +++ b/packages/luthor_generator/build.yaml @@ -5,3 +5,14 @@ builders: build_extensions: { ".dart": [".luthor.g.part"] } auto_apply: dependents build_to: cache + applies_builders: ["source_gen:combining_builder"] + +targets: + $default: + sources: + - lib/** + - test/** + - $package$ + builders: + luthor_generator:luthor: + enabled: true diff --git a/packages/luthor_generator/example/build.yaml b/packages/luthor_generator/example/build.yaml new file mode 100644 index 00000000..b9b54c38 --- /dev/null +++ b/packages/luthor_generator/example/build.yaml @@ -0,0 +1,5 @@ +targets: + $default: + builders: + luthor_generator:luthor: + enabled: true diff --git a/packages/luthor_generator/example/lib/sample.g.dart b/packages/luthor_generator/example/lib/sample.g.dart index 8d3e2ad8..79ded22a 100644 --- a/packages/luthor_generator/example/lib/sample.g.dart +++ b/packages/luthor_generator/example/lib/sample.g.dart @@ -137,9 +137,9 @@ Validator $SampleSchema = l.withName('Sample').schema({ messageFn: lengthErrorMessage, ), SampleSchemaKeys.minAndMaxString: l.string().max(200).min(8).required(), - SampleSchemaKeys.startsWithFoo: l.string().startsWith(r"foo").required(), - SampleSchemaKeys.endsWithBar: l.string().endsWith(r"bar").required(), - SampleSchemaKeys.containsBaz: l.string().contains(r"baz").required(), + SampleSchemaKeys.startsWithFoo: l.string().startsWith("foo").required(), + SampleSchemaKeys.endsWithBar: l.string().endsWith("bar").required(), + SampleSchemaKeys.containsBaz: l.string().contains("baz").required(), SampleSchemaKeys.minAndMaxInt: l.int().max(4).min(2).required(), SampleSchemaKeys.minAndMaxDouble: l.double().max(4.0).min(2.0).required(), SampleSchemaKeys.minAndMaxNumber: l.number().max(3.0).min(2).required(), diff --git a/packages/luthor_generator/lib/helpers/validations/string_validations.dart b/packages/luthor_generator/lib/helpers/validations/string_validations.dart index b77c7bef..8b113849 100644 --- a/packages/luthor_generator/lib/helpers/validations/string_validations.dart +++ b/packages/luthor_generator/lib/helpers/validations/string_validations.dart @@ -4,6 +4,19 @@ import 'package:analyzer/dart/element/element2.dart'; import 'package:luthor_generator/checkers.dart'; import 'package:luthor_generator/helpers/validations/base_validations.dart'; +/// Returns true if the string should be a raw string (contains backslashes) +bool _shouldUseRawString(String value) { + return value.contains(r'\'); +} + +/// Formats a string literal, using raw string only if needed +String _formatStringLiteral(String value) { + if (_shouldUseRawString(value)) { + return 'r"$value"'; + } + return '"$value"'; +} + String getStringValidations(FormalParameterElement param) { final buffer = StringBuffer(); @@ -239,7 +252,7 @@ void _checkAndWriteStartsWithValidation( .getField('messageFn') ?.toFunctionValue(); - final params = ['r"$string"']; + final params = [_formatStringLiteral(string!)]; if (message != null) params.add("message: '$message'"); if (messageFn != null) { params.add("messageFn: ${getQualifiedFunctionName(messageFn)}"); @@ -264,7 +277,7 @@ void _checkAndWriteEndsWithValidation( .getField('messageFn') ?.toFunctionValue(); - final params = ['r"$string"']; + final params = [_formatStringLiteral(string!)]; if (message != null) params.add("message: '$message'"); if (messageFn != null) { params.add("messageFn: ${getQualifiedFunctionName(messageFn)}"); @@ -289,7 +302,7 @@ void _checkAndWriteContainsValidation( .getField('messageFn') ?.toFunctionValue(); - final params = ['r"$string"']; + final params = [_formatStringLiteral(string!)]; if (message != null) params.add("message: '$message'"); if (messageFn != null) { params.add("messageFn: ${getQualifiedFunctionName(messageFn)}"); diff --git a/packages/luthor_generator/pubspec.yaml b/packages/luthor_generator/pubspec.yaml index ee4c92bf..26fc886d 100644 --- a/packages/luthor_generator/pubspec.yaml +++ b/packages/luthor_generator/pubspec.yaml @@ -19,5 +19,8 @@ dependencies: source_gen: ^4.0.0 # Configured via catalog dev_dependencies: + build_runner: ^2.4.0 + freezed: ^3.0.3 + json_serializable: ^6.7.0 lint: ^2.3.0 test: ^1.25.14 diff --git a/packages/luthor_generator/test/custom_validator_test.dart b/packages/luthor_generator/test/custom_validator_test.dart new file mode 100644 index 00000000..bd966618 --- /dev/null +++ b/packages/luthor_generator/test/custom_validator_test.dart @@ -0,0 +1,97 @@ +import 'package:luthor/luthor.dart'; +import 'package:test/test.dart'; + +import 'fixtures/custom_validator_model.dart'; + +void main() { + group('Custom Validator Tests', () { + test('should generate schema with custom validators', () { + expect($CustomValidatorModelSchema, isA()); + }); + + test('should validate @WithCustomValidator - valid case', () { + final validData = { + 'customField': 'valid', + 'matchField': 'test', + 'confirmField': 'test', + }; + + final result = $CustomValidatorModelValidate(validData); + + switch (result) { + case SchemaValidationSuccess(data: final model): + expect(model.customField, equals('valid')); + case SchemaValidationError(errors: final errors): + fail('Should not have validation errors: $errors'); + } + }); + + test( + 'should validate @WithCustomValidator - invalid case with custom message', + () { + final invalidData = { + 'customField': 'invalid', + 'matchField': 'test', + 'confirmField': 'test', + }; + + final result = $CustomValidatorModelValidate(invalidData); + + switch (result) { + case SchemaValidationSuccess(data: _): + fail('Should have validation error for custom validator'); + case SchemaValidationError(errors: final errors): + expect(errors.keys, contains('customField')); + expect(errors['customField'], isNotNull); + expect( + (errors['customField']! as List).first, + equals('Value must be "valid"'), + ); + } + }, + ); + + test('should validate @WithSchemaCustomValidator - matching fields', () { + final validData = { + 'customField': 'valid', + 'matchField': 'password123', + 'confirmField': 'password123', + }; + + final result = $CustomValidatorModelValidate(validData); + + switch (result) { + case SchemaValidationSuccess(data: final model): + expect(model.matchField, equals('password123')); + expect(model.confirmField, equals('password123')); + case SchemaValidationError(errors: final errors): + fail('Should not have validation errors: $errors'); + } + }); + + test( + 'should validate @WithSchemaCustomValidator - non-matching fields', + () { + final invalidData = { + 'customField': 'valid', + 'matchField': 'password123', + 'confirmField': 'different', + }; + + final result = $CustomValidatorModelValidate(invalidData); + + switch (result) { + case SchemaValidationSuccess(data: _): + fail('Should have validation error for non-matching fields'); + case SchemaValidationError(errors: final errors): + expect(errors.keys, contains('confirmField')); + expect(errors['confirmField'], isNotNull); + expect( + (errors['confirmField']! as List).first, + equals('Fields must match'), + ); + } + }, + ); + }); +} diff --git a/packages/luthor_generator/test/fixtures/annotated_model.dart b/packages/luthor_generator/test/fixtures/annotated_model.dart new file mode 100644 index 00000000..0373e0f3 --- /dev/null +++ b/packages/luthor_generator/test/fixtures/annotated_model.dart @@ -0,0 +1,23 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:luthor/luthor.dart'; + +part 'annotated_model.freezed.dart'; +part 'annotated_model.g.dart'; + +@luthor +@freezed +abstract class AnnotatedModel with _$AnnotatedModel { + const factory AnnotatedModel({ + @IsEmail() required String email, + @HasMin(8) @HasMax(100) required String password, + @HasMin(18) @HasMax(120) required int age, + @HasMinDouble(0.0) @HasMaxDouble(100.0) required double score, + @StartsWith('https://') String? website, + @EndsWith('.com') String? domain, + @Contains('test') String? testField, + @IsUuid() required String uuid, + }) = _AnnotatedModel; + + factory AnnotatedModel.fromJson(Map json) => + _$AnnotatedModelFromJson(json); +} diff --git a/packages/luthor_generator/test/fixtures/annotated_model.freezed.dart b/packages/luthor_generator/test/fixtures/annotated_model.freezed.dart new file mode 100644 index 00000000..5de5005f --- /dev/null +++ b/packages/luthor_generator/test/fixtures/annotated_model.freezed.dart @@ -0,0 +1,298 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'annotated_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$AnnotatedModel { + +@IsEmail() String get email;@HasMin(8)@HasMax(100) String get password;@HasMin(18)@HasMax(120) int get age;@HasMinDouble(0.0)@HasMaxDouble(100.0) double get score;@StartsWith('https://') String? get website;@EndsWith('.com') String? get domain;@Contains('test') String? get testField;@IsUuid() String get uuid; +/// Create a copy of AnnotatedModel +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$AnnotatedModelCopyWith get copyWith => _$AnnotatedModelCopyWithImpl(this as AnnotatedModel, _$identity); + + /// Serializes this AnnotatedModel to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is AnnotatedModel&&(identical(other.email, email) || other.email == email)&&(identical(other.password, password) || other.password == password)&&(identical(other.age, age) || other.age == age)&&(identical(other.score, score) || other.score == score)&&(identical(other.website, website) || other.website == website)&&(identical(other.domain, domain) || other.domain == domain)&&(identical(other.testField, testField) || other.testField == testField)&&(identical(other.uuid, uuid) || other.uuid == uuid)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,email,password,age,score,website,domain,testField,uuid); + +@override +String toString() { + return 'AnnotatedModel(email: $email, password: $password, age: $age, score: $score, website: $website, domain: $domain, testField: $testField, uuid: $uuid)'; +} + + +} + +/// @nodoc +abstract mixin class $AnnotatedModelCopyWith<$Res> { + factory $AnnotatedModelCopyWith(AnnotatedModel value, $Res Function(AnnotatedModel) _then) = _$AnnotatedModelCopyWithImpl; +@useResult +$Res call({ +@IsEmail() String email,@HasMin(8)@HasMax(100) String password,@HasMin(18)@HasMax(120) int age,@HasMinDouble(0.0)@HasMaxDouble(100.0) double score,@StartsWith('https://') String? website,@EndsWith('.com') String? domain,@Contains('test') String? testField,@IsUuid() String uuid +}); + + + + +} +/// @nodoc +class _$AnnotatedModelCopyWithImpl<$Res> + implements $AnnotatedModelCopyWith<$Res> { + _$AnnotatedModelCopyWithImpl(this._self, this._then); + + final AnnotatedModel _self; + final $Res Function(AnnotatedModel) _then; + +/// Create a copy of AnnotatedModel +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? email = null,Object? password = null,Object? age = null,Object? score = null,Object? website = freezed,Object? domain = freezed,Object? testField = freezed,Object? uuid = null,}) { + return _then(_self.copyWith( +email: null == email ? _self.email : email // ignore: cast_nullable_to_non_nullable +as String,password: null == password ? _self.password : password // ignore: cast_nullable_to_non_nullable +as String,age: null == age ? _self.age : age // ignore: cast_nullable_to_non_nullable +as int,score: null == score ? _self.score : score // ignore: cast_nullable_to_non_nullable +as double,website: freezed == website ? _self.website : website // ignore: cast_nullable_to_non_nullable +as String?,domain: freezed == domain ? _self.domain : domain // ignore: cast_nullable_to_non_nullable +as String?,testField: freezed == testField ? _self.testField : testField // ignore: cast_nullable_to_non_nullable +as String?,uuid: null == uuid ? _self.uuid : uuid // ignore: cast_nullable_to_non_nullable +as String, + )); +} + +} + + +/// Adds pattern-matching-related methods to [AnnotatedModel]. +extension AnnotatedModelPatterns on AnnotatedModel { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _AnnotatedModel value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _AnnotatedModel() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _AnnotatedModel value) $default,){ +final _that = this; +switch (_that) { +case _AnnotatedModel(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _AnnotatedModel value)? $default,){ +final _that = this; +switch (_that) { +case _AnnotatedModel() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function(@IsEmail() String email, @HasMin(8)@HasMax(100) String password, @HasMin(18)@HasMax(120) int age, @HasMinDouble(0.0)@HasMaxDouble(100.0) double score, @StartsWith('https://') String? website, @EndsWith('.com') String? domain, @Contains('test') String? testField, @IsUuid() String uuid)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _AnnotatedModel() when $default != null: +return $default(_that.email,_that.password,_that.age,_that.score,_that.website,_that.domain,_that.testField,_that.uuid);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function(@IsEmail() String email, @HasMin(8)@HasMax(100) String password, @HasMin(18)@HasMax(120) int age, @HasMinDouble(0.0)@HasMaxDouble(100.0) double score, @StartsWith('https://') String? website, @EndsWith('.com') String? domain, @Contains('test') String? testField, @IsUuid() String uuid) $default,) {final _that = this; +switch (_that) { +case _AnnotatedModel(): +return $default(_that.email,_that.password,_that.age,_that.score,_that.website,_that.domain,_that.testField,_that.uuid);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function(@IsEmail() String email, @HasMin(8)@HasMax(100) String password, @HasMin(18)@HasMax(120) int age, @HasMinDouble(0.0)@HasMaxDouble(100.0) double score, @StartsWith('https://') String? website, @EndsWith('.com') String? domain, @Contains('test') String? testField, @IsUuid() String uuid)? $default,) {final _that = this; +switch (_that) { +case _AnnotatedModel() when $default != null: +return $default(_that.email,_that.password,_that.age,_that.score,_that.website,_that.domain,_that.testField,_that.uuid);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _AnnotatedModel implements AnnotatedModel { + const _AnnotatedModel({@IsEmail() required this.email, @HasMin(8)@HasMax(100) required this.password, @HasMin(18)@HasMax(120) required this.age, @HasMinDouble(0.0)@HasMaxDouble(100.0) required this.score, @StartsWith('https://') this.website, @EndsWith('.com') this.domain, @Contains('test') this.testField, @IsUuid() required this.uuid}); + factory _AnnotatedModel.fromJson(Map json) => _$AnnotatedModelFromJson(json); + +@override@IsEmail() final String email; +@override@HasMin(8)@HasMax(100) final String password; +@override@HasMin(18)@HasMax(120) final int age; +@override@HasMinDouble(0.0)@HasMaxDouble(100.0) final double score; +@override@StartsWith('https://') final String? website; +@override@EndsWith('.com') final String? domain; +@override@Contains('test') final String? testField; +@override@IsUuid() final String uuid; + +/// Create a copy of AnnotatedModel +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$AnnotatedModelCopyWith<_AnnotatedModel> get copyWith => __$AnnotatedModelCopyWithImpl<_AnnotatedModel>(this, _$identity); + +@override +Map toJson() { + return _$AnnotatedModelToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _AnnotatedModel&&(identical(other.email, email) || other.email == email)&&(identical(other.password, password) || other.password == password)&&(identical(other.age, age) || other.age == age)&&(identical(other.score, score) || other.score == score)&&(identical(other.website, website) || other.website == website)&&(identical(other.domain, domain) || other.domain == domain)&&(identical(other.testField, testField) || other.testField == testField)&&(identical(other.uuid, uuid) || other.uuid == uuid)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,email,password,age,score,website,domain,testField,uuid); + +@override +String toString() { + return 'AnnotatedModel(email: $email, password: $password, age: $age, score: $score, website: $website, domain: $domain, testField: $testField, uuid: $uuid)'; +} + + +} + +/// @nodoc +abstract mixin class _$AnnotatedModelCopyWith<$Res> implements $AnnotatedModelCopyWith<$Res> { + factory _$AnnotatedModelCopyWith(_AnnotatedModel value, $Res Function(_AnnotatedModel) _then) = __$AnnotatedModelCopyWithImpl; +@override @useResult +$Res call({ +@IsEmail() String email,@HasMin(8)@HasMax(100) String password,@HasMin(18)@HasMax(120) int age,@HasMinDouble(0.0)@HasMaxDouble(100.0) double score,@StartsWith('https://') String? website,@EndsWith('.com') String? domain,@Contains('test') String? testField,@IsUuid() String uuid +}); + + + + +} +/// @nodoc +class __$AnnotatedModelCopyWithImpl<$Res> + implements _$AnnotatedModelCopyWith<$Res> { + __$AnnotatedModelCopyWithImpl(this._self, this._then); + + final _AnnotatedModel _self; + final $Res Function(_AnnotatedModel) _then; + +/// Create a copy of AnnotatedModel +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? email = null,Object? password = null,Object? age = null,Object? score = null,Object? website = freezed,Object? domain = freezed,Object? testField = freezed,Object? uuid = null,}) { + return _then(_AnnotatedModel( +email: null == email ? _self.email : email // ignore: cast_nullable_to_non_nullable +as String,password: null == password ? _self.password : password // ignore: cast_nullable_to_non_nullable +as String,age: null == age ? _self.age : age // ignore: cast_nullable_to_non_nullable +as int,score: null == score ? _self.score : score // ignore: cast_nullable_to_non_nullable +as double,website: freezed == website ? _self.website : website // ignore: cast_nullable_to_non_nullable +as String?,domain: freezed == domain ? _self.domain : domain // ignore: cast_nullable_to_non_nullable +as String?,testField: freezed == testField ? _self.testField : testField // ignore: cast_nullable_to_non_nullable +as String?,uuid: null == uuid ? _self.uuid : uuid // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +// dart format on diff --git a/packages/luthor_generator/test/fixtures/annotated_model.g.dart b/packages/luthor_generator/test/fixtures/annotated_model.g.dart new file mode 100644 index 00000000..28f5c0d6 --- /dev/null +++ b/packages/luthor_generator/test/fixtures/annotated_model.g.dart @@ -0,0 +1,82 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'annotated_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_AnnotatedModel _$AnnotatedModelFromJson(Map json) => + _AnnotatedModel( + email: json['email'] as String, + password: json['password'] as String, + age: (json['age'] as num).toInt(), + score: (json['score'] as num).toDouble(), + website: json['website'] as String?, + domain: json['domain'] as String?, + testField: json['testField'] as String?, + uuid: json['uuid'] as String, + ); + +Map _$AnnotatedModelToJson(_AnnotatedModel instance) => + { + 'email': instance.email, + 'password': instance.password, + 'age': instance.age, + 'score': instance.score, + 'website': instance.website, + 'domain': instance.domain, + 'testField': instance.testField, + 'uuid': instance.uuid, + }; + +// ************************************************************************** +// LuthorGenerator +// ************************************************************************** + +// ignore: constant_identifier_names +const AnnotatedModelSchemaKeys = ( + email: "email", + password: "password", + age: "age", + score: "score", + website: "website", + domain: "domain", + testField: "testField", + uuid: "uuid", +); + +Validator $AnnotatedModelSchema = l.withName('AnnotatedModel').schema({ + AnnotatedModelSchemaKeys.email: l.string().email().required(), + AnnotatedModelSchemaKeys.password: l.string().max(100).min(8).required(), + AnnotatedModelSchemaKeys.age: l.int().max(120).min(18).required(), + AnnotatedModelSchemaKeys.score: l.double().max(100.0).min(0.0).required(), + AnnotatedModelSchemaKeys.website: l.string().startsWith("https://"), + AnnotatedModelSchemaKeys.domain: l.string().endsWith(".com"), + AnnotatedModelSchemaKeys.testField: l.string().contains("test"), + AnnotatedModelSchemaKeys.uuid: l.string().uuid().required(), +}); + +SchemaValidationResult $AnnotatedModelValidate( + Map json, +) => $AnnotatedModelSchema.validateSchema( + json, + fromJson: AnnotatedModel.fromJson, +); + +extension AnnotatedModelValidationExtension on AnnotatedModel { + SchemaValidationResult validateSelf() => + $AnnotatedModelValidate(toJson()); +} + +// ignore: constant_identifier_names +const AnnotatedModelErrorKeys = ( + email: "email", + password: "password", + age: "age", + score: "score", + website: "website", + domain: "domain", + testField: "testField", + uuid: "uuid", +); diff --git a/packages/luthor_generator/test/fixtures/custom_validator_model.dart b/packages/luthor_generator/test/fixtures/custom_validator_model.dart new file mode 100644 index 00000000..f0fd0fcc --- /dev/null +++ b/packages/luthor_generator/test/fixtures/custom_validator_model.dart @@ -0,0 +1,32 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:luthor/luthor.dart'; + +part 'custom_validator_model.freezed.dart'; +part 'custom_validator_model.g.dart'; + +bool customValidatorFn(Object? value) { + return value == 'valid'; +} + +String customErrorMessage() { + return 'Value must be "valid"'; +} + +bool schemaValidatorFn(Object? value, Map data) { + return value == data['matchField']; +} + +@luthor +@freezed +abstract class CustomValidatorModel with _$CustomValidatorModel { + const factory CustomValidatorModel({ + @WithCustomValidator(customValidatorFn, messageFn: customErrorMessage) + required String customField, + required String matchField, + @WithSchemaCustomValidator(schemaValidatorFn, message: 'Fields must match') + required String confirmField, + }) = _CustomValidatorModel; + + factory CustomValidatorModel.fromJson(Map json) => + _$CustomValidatorModelFromJson(json); +} diff --git a/packages/luthor_generator/test/fixtures/custom_validator_model.freezed.dart b/packages/luthor_generator/test/fixtures/custom_validator_model.freezed.dart new file mode 100644 index 00000000..e615a6dc --- /dev/null +++ b/packages/luthor_generator/test/fixtures/custom_validator_model.freezed.dart @@ -0,0 +1,283 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'custom_validator_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$CustomValidatorModel { + +@WithCustomValidator(customValidatorFn, messageFn: customErrorMessage) String get customField; String get matchField;@WithSchemaCustomValidator(schemaValidatorFn, message: 'Fields must match') String get confirmField; +/// Create a copy of CustomValidatorModel +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$CustomValidatorModelCopyWith get copyWith => _$CustomValidatorModelCopyWithImpl(this as CustomValidatorModel, _$identity); + + /// Serializes this CustomValidatorModel to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is CustomValidatorModel&&(identical(other.customField, customField) || other.customField == customField)&&(identical(other.matchField, matchField) || other.matchField == matchField)&&(identical(other.confirmField, confirmField) || other.confirmField == confirmField)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,customField,matchField,confirmField); + +@override +String toString() { + return 'CustomValidatorModel(customField: $customField, matchField: $matchField, confirmField: $confirmField)'; +} + + +} + +/// @nodoc +abstract mixin class $CustomValidatorModelCopyWith<$Res> { + factory $CustomValidatorModelCopyWith(CustomValidatorModel value, $Res Function(CustomValidatorModel) _then) = _$CustomValidatorModelCopyWithImpl; +@useResult +$Res call({ +@WithCustomValidator(customValidatorFn, messageFn: customErrorMessage) String customField, String matchField,@WithSchemaCustomValidator(schemaValidatorFn, message: 'Fields must match') String confirmField +}); + + + + +} +/// @nodoc +class _$CustomValidatorModelCopyWithImpl<$Res> + implements $CustomValidatorModelCopyWith<$Res> { + _$CustomValidatorModelCopyWithImpl(this._self, this._then); + + final CustomValidatorModel _self; + final $Res Function(CustomValidatorModel) _then; + +/// Create a copy of CustomValidatorModel +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? customField = null,Object? matchField = null,Object? confirmField = null,}) { + return _then(_self.copyWith( +customField: null == customField ? _self.customField : customField // ignore: cast_nullable_to_non_nullable +as String,matchField: null == matchField ? _self.matchField : matchField // ignore: cast_nullable_to_non_nullable +as String,confirmField: null == confirmField ? _self.confirmField : confirmField // ignore: cast_nullable_to_non_nullable +as String, + )); +} + +} + + +/// Adds pattern-matching-related methods to [CustomValidatorModel]. +extension CustomValidatorModelPatterns on CustomValidatorModel { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _CustomValidatorModel value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _CustomValidatorModel() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _CustomValidatorModel value) $default,){ +final _that = this; +switch (_that) { +case _CustomValidatorModel(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _CustomValidatorModel value)? $default,){ +final _that = this; +switch (_that) { +case _CustomValidatorModel() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function(@WithCustomValidator(customValidatorFn, messageFn: customErrorMessage) String customField, String matchField, @WithSchemaCustomValidator(schemaValidatorFn, message: 'Fields must match') String confirmField)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _CustomValidatorModel() when $default != null: +return $default(_that.customField,_that.matchField,_that.confirmField);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function(@WithCustomValidator(customValidatorFn, messageFn: customErrorMessage) String customField, String matchField, @WithSchemaCustomValidator(schemaValidatorFn, message: 'Fields must match') String confirmField) $default,) {final _that = this; +switch (_that) { +case _CustomValidatorModel(): +return $default(_that.customField,_that.matchField,_that.confirmField);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function(@WithCustomValidator(customValidatorFn, messageFn: customErrorMessage) String customField, String matchField, @WithSchemaCustomValidator(schemaValidatorFn, message: 'Fields must match') String confirmField)? $default,) {final _that = this; +switch (_that) { +case _CustomValidatorModel() when $default != null: +return $default(_that.customField,_that.matchField,_that.confirmField);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _CustomValidatorModel implements CustomValidatorModel { + const _CustomValidatorModel({@WithCustomValidator(customValidatorFn, messageFn: customErrorMessage) required this.customField, required this.matchField, @WithSchemaCustomValidator(schemaValidatorFn, message: 'Fields must match') required this.confirmField}); + factory _CustomValidatorModel.fromJson(Map json) => _$CustomValidatorModelFromJson(json); + +@override@WithCustomValidator(customValidatorFn, messageFn: customErrorMessage) final String customField; +@override final String matchField; +@override@WithSchemaCustomValidator(schemaValidatorFn, message: 'Fields must match') final String confirmField; + +/// Create a copy of CustomValidatorModel +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$CustomValidatorModelCopyWith<_CustomValidatorModel> get copyWith => __$CustomValidatorModelCopyWithImpl<_CustomValidatorModel>(this, _$identity); + +@override +Map toJson() { + return _$CustomValidatorModelToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _CustomValidatorModel&&(identical(other.customField, customField) || other.customField == customField)&&(identical(other.matchField, matchField) || other.matchField == matchField)&&(identical(other.confirmField, confirmField) || other.confirmField == confirmField)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,customField,matchField,confirmField); + +@override +String toString() { + return 'CustomValidatorModel(customField: $customField, matchField: $matchField, confirmField: $confirmField)'; +} + + +} + +/// @nodoc +abstract mixin class _$CustomValidatorModelCopyWith<$Res> implements $CustomValidatorModelCopyWith<$Res> { + factory _$CustomValidatorModelCopyWith(_CustomValidatorModel value, $Res Function(_CustomValidatorModel) _then) = __$CustomValidatorModelCopyWithImpl; +@override @useResult +$Res call({ +@WithCustomValidator(customValidatorFn, messageFn: customErrorMessage) String customField, String matchField,@WithSchemaCustomValidator(schemaValidatorFn, message: 'Fields must match') String confirmField +}); + + + + +} +/// @nodoc +class __$CustomValidatorModelCopyWithImpl<$Res> + implements _$CustomValidatorModelCopyWith<$Res> { + __$CustomValidatorModelCopyWithImpl(this._self, this._then); + + final _CustomValidatorModel _self; + final $Res Function(_CustomValidatorModel) _then; + +/// Create a copy of CustomValidatorModel +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? customField = null,Object? matchField = null,Object? confirmField = null,}) { + return _then(_CustomValidatorModel( +customField: null == customField ? _self.customField : customField // ignore: cast_nullable_to_non_nullable +as String,matchField: null == matchField ? _self.matchField : matchField // ignore: cast_nullable_to_non_nullable +as String,confirmField: null == confirmField ? _self.confirmField : confirmField // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +// dart format on diff --git a/packages/luthor_generator/test/fixtures/custom_validator_model.g.dart b/packages/luthor_generator/test/fixtures/custom_validator_model.g.dart new file mode 100644 index 00000000..2b5c4231 --- /dev/null +++ b/packages/luthor_generator/test/fixtures/custom_validator_model.g.dart @@ -0,0 +1,67 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'custom_validator_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_CustomValidatorModel _$CustomValidatorModelFromJson( + Map json, +) => _CustomValidatorModel( + customField: json['customField'] as String, + matchField: json['matchField'] as String, + confirmField: json['confirmField'] as String, +); + +Map _$CustomValidatorModelToJson( + _CustomValidatorModel instance, +) => { + 'customField': instance.customField, + 'matchField': instance.matchField, + 'confirmField': instance.confirmField, +}; + +// ************************************************************************** +// LuthorGenerator +// ************************************************************************** + +// ignore: constant_identifier_names +const CustomValidatorModelSchemaKeys = ( + customField: "customField", + matchField: "matchField", + confirmField: "confirmField", +); + +Validator $CustomValidatorModelSchema = l + .withName('CustomValidatorModel') + .schema({ + CustomValidatorModelSchemaKeys.customField: l + .string() + .custom(customValidatorFn, messageFn: customErrorMessage) + .required(), + CustomValidatorModelSchemaKeys.matchField: l.string().required(), + CustomValidatorModelSchemaKeys.confirmField: l + .string() + .customWithSchema(schemaValidatorFn, message: 'Fields must match') + .required(), + }); + +SchemaValidationResult $CustomValidatorModelValidate( + Map json, +) => $CustomValidatorModelSchema.validateSchema( + json, + fromJson: CustomValidatorModel.fromJson, +); + +extension CustomValidatorModelValidationExtension on CustomValidatorModel { + SchemaValidationResult validateSelf() => + $CustomValidatorModelValidate(toJson()); +} + +// ignore: constant_identifier_names +const CustomValidatorModelErrorKeys = ( + customField: "customField", + matchField: "matchField", + confirmField: "confirmField", +); diff --git a/packages/luthor_generator/test/fixtures/json_key_model.dart b/packages/luthor_generator/test/fixtures/json_key_model.dart new file mode 100644 index 00000000..07695abf --- /dev/null +++ b/packages/luthor_generator/test/fixtures/json_key_model.dart @@ -0,0 +1,20 @@ +// ignore_for_file: invalid_annotation_target + +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:luthor/luthor.dart'; + +part 'json_key_model.freezed.dart'; +part 'json_key_model.g.dart'; + +@luthor +@freezed +abstract class JsonKeyModel with _$JsonKeyModel { + const factory JsonKeyModel({ + @JsonKey(name: 'user_name') required String userName, + @JsonKey(name: 'user_email') @IsEmail() required String userEmail, + required String normalField, + }) = _JsonKeyModel; + + factory JsonKeyModel.fromJson(Map json) => + _$JsonKeyModelFromJson(json); +} diff --git a/packages/luthor_generator/test/fixtures/json_key_model.freezed.dart b/packages/luthor_generator/test/fixtures/json_key_model.freezed.dart new file mode 100644 index 00000000..2f1c02a9 --- /dev/null +++ b/packages/luthor_generator/test/fixtures/json_key_model.freezed.dart @@ -0,0 +1,283 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'json_key_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$JsonKeyModel { + +@JsonKey(name: 'user_name') String get userName;@JsonKey(name: 'user_email')@IsEmail() String get userEmail; String get normalField; +/// Create a copy of JsonKeyModel +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$JsonKeyModelCopyWith get copyWith => _$JsonKeyModelCopyWithImpl(this as JsonKeyModel, _$identity); + + /// Serializes this JsonKeyModel to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is JsonKeyModel&&(identical(other.userName, userName) || other.userName == userName)&&(identical(other.userEmail, userEmail) || other.userEmail == userEmail)&&(identical(other.normalField, normalField) || other.normalField == normalField)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,userName,userEmail,normalField); + +@override +String toString() { + return 'JsonKeyModel(userName: $userName, userEmail: $userEmail, normalField: $normalField)'; +} + + +} + +/// @nodoc +abstract mixin class $JsonKeyModelCopyWith<$Res> { + factory $JsonKeyModelCopyWith(JsonKeyModel value, $Res Function(JsonKeyModel) _then) = _$JsonKeyModelCopyWithImpl; +@useResult +$Res call({ +@JsonKey(name: 'user_name') String userName,@JsonKey(name: 'user_email')@IsEmail() String userEmail, String normalField +}); + + + + +} +/// @nodoc +class _$JsonKeyModelCopyWithImpl<$Res> + implements $JsonKeyModelCopyWith<$Res> { + _$JsonKeyModelCopyWithImpl(this._self, this._then); + + final JsonKeyModel _self; + final $Res Function(JsonKeyModel) _then; + +/// Create a copy of JsonKeyModel +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? userName = null,Object? userEmail = null,Object? normalField = null,}) { + return _then(_self.copyWith( +userName: null == userName ? _self.userName : userName // ignore: cast_nullable_to_non_nullable +as String,userEmail: null == userEmail ? _self.userEmail : userEmail // ignore: cast_nullable_to_non_nullable +as String,normalField: null == normalField ? _self.normalField : normalField // ignore: cast_nullable_to_non_nullable +as String, + )); +} + +} + + +/// Adds pattern-matching-related methods to [JsonKeyModel]. +extension JsonKeyModelPatterns on JsonKeyModel { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _JsonKeyModel value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _JsonKeyModel() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _JsonKeyModel value) $default,){ +final _that = this; +switch (_that) { +case _JsonKeyModel(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _JsonKeyModel value)? $default,){ +final _that = this; +switch (_that) { +case _JsonKeyModel() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function(@JsonKey(name: 'user_name') String userName, @JsonKey(name: 'user_email')@IsEmail() String userEmail, String normalField)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _JsonKeyModel() when $default != null: +return $default(_that.userName,_that.userEmail,_that.normalField);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function(@JsonKey(name: 'user_name') String userName, @JsonKey(name: 'user_email')@IsEmail() String userEmail, String normalField) $default,) {final _that = this; +switch (_that) { +case _JsonKeyModel(): +return $default(_that.userName,_that.userEmail,_that.normalField);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function(@JsonKey(name: 'user_name') String userName, @JsonKey(name: 'user_email')@IsEmail() String userEmail, String normalField)? $default,) {final _that = this; +switch (_that) { +case _JsonKeyModel() when $default != null: +return $default(_that.userName,_that.userEmail,_that.normalField);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _JsonKeyModel implements JsonKeyModel { + const _JsonKeyModel({@JsonKey(name: 'user_name') required this.userName, @JsonKey(name: 'user_email')@IsEmail() required this.userEmail, required this.normalField}); + factory _JsonKeyModel.fromJson(Map json) => _$JsonKeyModelFromJson(json); + +@override@JsonKey(name: 'user_name') final String userName; +@override@JsonKey(name: 'user_email')@IsEmail() final String userEmail; +@override final String normalField; + +/// Create a copy of JsonKeyModel +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$JsonKeyModelCopyWith<_JsonKeyModel> get copyWith => __$JsonKeyModelCopyWithImpl<_JsonKeyModel>(this, _$identity); + +@override +Map toJson() { + return _$JsonKeyModelToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _JsonKeyModel&&(identical(other.userName, userName) || other.userName == userName)&&(identical(other.userEmail, userEmail) || other.userEmail == userEmail)&&(identical(other.normalField, normalField) || other.normalField == normalField)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,userName,userEmail,normalField); + +@override +String toString() { + return 'JsonKeyModel(userName: $userName, userEmail: $userEmail, normalField: $normalField)'; +} + + +} + +/// @nodoc +abstract mixin class _$JsonKeyModelCopyWith<$Res> implements $JsonKeyModelCopyWith<$Res> { + factory _$JsonKeyModelCopyWith(_JsonKeyModel value, $Res Function(_JsonKeyModel) _then) = __$JsonKeyModelCopyWithImpl; +@override @useResult +$Res call({ +@JsonKey(name: 'user_name') String userName,@JsonKey(name: 'user_email')@IsEmail() String userEmail, String normalField +}); + + + + +} +/// @nodoc +class __$JsonKeyModelCopyWithImpl<$Res> + implements _$JsonKeyModelCopyWith<$Res> { + __$JsonKeyModelCopyWithImpl(this._self, this._then); + + final _JsonKeyModel _self; + final $Res Function(_JsonKeyModel) _then; + +/// Create a copy of JsonKeyModel +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? userName = null,Object? userEmail = null,Object? normalField = null,}) { + return _then(_JsonKeyModel( +userName: null == userName ? _self.userName : userName // ignore: cast_nullable_to_non_nullable +as String,userEmail: null == userEmail ? _self.userEmail : userEmail // ignore: cast_nullable_to_non_nullable +as String,normalField: null == normalField ? _self.normalField : normalField // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +// dart format on diff --git a/packages/luthor_generator/test/fixtures/json_key_model.g.dart b/packages/luthor_generator/test/fixtures/json_key_model.g.dart new file mode 100644 index 00000000..7c5b1c66 --- /dev/null +++ b/packages/luthor_generator/test/fixtures/json_key_model.g.dart @@ -0,0 +1,54 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'json_key_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_JsonKeyModel _$JsonKeyModelFromJson(Map json) => + _JsonKeyModel( + userName: json['user_name'] as String, + userEmail: json['user_email'] as String, + normalField: json['normalField'] as String, + ); + +Map _$JsonKeyModelToJson(_JsonKeyModel instance) => + { + 'user_name': instance.userName, + 'user_email': instance.userEmail, + 'normalField': instance.normalField, + }; + +// ************************************************************************** +// LuthorGenerator +// ************************************************************************** + +// ignore: constant_identifier_names +const JsonKeyModelSchemaKeys = ( + userName: "user_name", + userEmail: "user_email", + normalField: "normalField", +); + +Validator $JsonKeyModelSchema = l.withName('JsonKeyModel').schema({ + JsonKeyModelSchemaKeys.userName: l.string().required(), + JsonKeyModelSchemaKeys.userEmail: l.string().email().required(), + JsonKeyModelSchemaKeys.normalField: l.string().required(), +}); + +SchemaValidationResult $JsonKeyModelValidate( + Map json, +) => $JsonKeyModelSchema.validateSchema(json, fromJson: JsonKeyModel.fromJson); + +extension JsonKeyModelValidationExtension on JsonKeyModel { + SchemaValidationResult validateSelf() => + $JsonKeyModelValidate(toJson()); +} + +// ignore: constant_identifier_names +const JsonKeyModelErrorKeys = ( + userName: "user_name", + userEmail: "user_email", + normalField: "normalField", +); diff --git a/packages/luthor_generator/test/fixtures/list_model.dart b/packages/luthor_generator/test/fixtures/list_model.dart new file mode 100644 index 00000000..b1fface8 --- /dev/null +++ b/packages/luthor_generator/test/fixtures/list_model.dart @@ -0,0 +1,18 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:luthor/luthor.dart'; + +part 'list_model.freezed.dart'; +part 'list_model.g.dart'; + +@luthor +@freezed +abstract class ListModel with _$ListModel { + const factory ListModel({ + required List stringList, + required List intList, + List? optionalList, + }) = _ListModel; + + factory ListModel.fromJson(Map json) => + _$ListModelFromJson(json); +} diff --git a/packages/luthor_generator/test/fixtures/list_model.freezed.dart b/packages/luthor_generator/test/fixtures/list_model.freezed.dart new file mode 100644 index 00000000..e4ece0cc --- /dev/null +++ b/packages/luthor_generator/test/fixtures/list_model.freezed.dart @@ -0,0 +1,303 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'list_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$ListModel { + + List get stringList; List get intList; List? get optionalList; +/// Create a copy of ListModel +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ListModelCopyWith get copyWith => _$ListModelCopyWithImpl(this as ListModel, _$identity); + + /// Serializes this ListModel to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ListModel&&const DeepCollectionEquality().equals(other.stringList, stringList)&&const DeepCollectionEquality().equals(other.intList, intList)&&const DeepCollectionEquality().equals(other.optionalList, optionalList)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(stringList),const DeepCollectionEquality().hash(intList),const DeepCollectionEquality().hash(optionalList)); + +@override +String toString() { + return 'ListModel(stringList: $stringList, intList: $intList, optionalList: $optionalList)'; +} + + +} + +/// @nodoc +abstract mixin class $ListModelCopyWith<$Res> { + factory $ListModelCopyWith(ListModel value, $Res Function(ListModel) _then) = _$ListModelCopyWithImpl; +@useResult +$Res call({ + List stringList, List intList, List? optionalList +}); + + + + +} +/// @nodoc +class _$ListModelCopyWithImpl<$Res> + implements $ListModelCopyWith<$Res> { + _$ListModelCopyWithImpl(this._self, this._then); + + final ListModel _self; + final $Res Function(ListModel) _then; + +/// Create a copy of ListModel +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? stringList = null,Object? intList = null,Object? optionalList = freezed,}) { + return _then(_self.copyWith( +stringList: null == stringList ? _self.stringList : stringList // ignore: cast_nullable_to_non_nullable +as List,intList: null == intList ? _self.intList : intList // ignore: cast_nullable_to_non_nullable +as List,optionalList: freezed == optionalList ? _self.optionalList : optionalList // ignore: cast_nullable_to_non_nullable +as List?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ListModel]. +extension ListModelPatterns on ListModel { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ListModel value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ListModel() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ListModel value) $default,){ +final _that = this; +switch (_that) { +case _ListModel(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ListModel value)? $default,){ +final _that = this; +switch (_that) { +case _ListModel() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( List stringList, List intList, List? optionalList)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ListModel() when $default != null: +return $default(_that.stringList,_that.intList,_that.optionalList);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( List stringList, List intList, List? optionalList) $default,) {final _that = this; +switch (_that) { +case _ListModel(): +return $default(_that.stringList,_that.intList,_that.optionalList);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( List stringList, List intList, List? optionalList)? $default,) {final _that = this; +switch (_that) { +case _ListModel() when $default != null: +return $default(_that.stringList,_that.intList,_that.optionalList);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _ListModel implements ListModel { + const _ListModel({required final List stringList, required final List intList, final List? optionalList}): _stringList = stringList,_intList = intList,_optionalList = optionalList; + factory _ListModel.fromJson(Map json) => _$ListModelFromJson(json); + + final List _stringList; +@override List get stringList { + if (_stringList is EqualUnmodifiableListView) return _stringList; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_stringList); +} + + final List _intList; +@override List get intList { + if (_intList is EqualUnmodifiableListView) return _intList; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_intList); +} + + final List? _optionalList; +@override List? get optionalList { + final value = _optionalList; + if (value == null) return null; + if (_optionalList is EqualUnmodifiableListView) return _optionalList; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); +} + + +/// Create a copy of ListModel +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ListModelCopyWith<_ListModel> get copyWith => __$ListModelCopyWithImpl<_ListModel>(this, _$identity); + +@override +Map toJson() { + return _$ListModelToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ListModel&&const DeepCollectionEquality().equals(other._stringList, _stringList)&&const DeepCollectionEquality().equals(other._intList, _intList)&&const DeepCollectionEquality().equals(other._optionalList, _optionalList)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_stringList),const DeepCollectionEquality().hash(_intList),const DeepCollectionEquality().hash(_optionalList)); + +@override +String toString() { + return 'ListModel(stringList: $stringList, intList: $intList, optionalList: $optionalList)'; +} + + +} + +/// @nodoc +abstract mixin class _$ListModelCopyWith<$Res> implements $ListModelCopyWith<$Res> { + factory _$ListModelCopyWith(_ListModel value, $Res Function(_ListModel) _then) = __$ListModelCopyWithImpl; +@override @useResult +$Res call({ + List stringList, List intList, List? optionalList +}); + + + + +} +/// @nodoc +class __$ListModelCopyWithImpl<$Res> + implements _$ListModelCopyWith<$Res> { + __$ListModelCopyWithImpl(this._self, this._then); + + final _ListModel _self; + final $Res Function(_ListModel) _then; + +/// Create a copy of ListModel +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? stringList = null,Object? intList = null,Object? optionalList = freezed,}) { + return _then(_ListModel( +stringList: null == stringList ? _self._stringList : stringList // ignore: cast_nullable_to_non_nullable +as List,intList: null == intList ? _self._intList : intList // ignore: cast_nullable_to_non_nullable +as List,optionalList: freezed == optionalList ? _self._optionalList : optionalList // ignore: cast_nullable_to_non_nullable +as List?, + )); +} + + +} + +// dart format on diff --git a/packages/luthor_generator/test/fixtures/list_model.g.dart b/packages/luthor_generator/test/fixtures/list_model.g.dart new file mode 100644 index 00000000..73715529 --- /dev/null +++ b/packages/luthor_generator/test/fixtures/list_model.g.dart @@ -0,0 +1,63 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'list_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_ListModel _$ListModelFromJson(Map json) => _ListModel( + stringList: (json['stringList'] as List) + .map((e) => e as String) + .toList(), + intList: (json['intList'] as List) + .map((e) => (e as num).toInt()) + .toList(), + optionalList: (json['optionalList'] as List?) + ?.map((e) => e as String) + .toList(), +); + +Map _$ListModelToJson(_ListModel instance) => + { + 'stringList': instance.stringList, + 'intList': instance.intList, + 'optionalList': instance.optionalList, + }; + +// ************************************************************************** +// LuthorGenerator +// ************************************************************************** + +// ignore: constant_identifier_names +const ListModelSchemaKeys = ( + stringList: "stringList", + intList: "intList", + optionalList: "optionalList", +); + +Validator $ListModelSchema = l.withName('ListModel').schema({ + ListModelSchemaKeys.stringList: l + .list(validators: [l.string().required()]) + .required(), + ListModelSchemaKeys.intList: l + .list(validators: [l.int().required()]) + .required(), + ListModelSchemaKeys.optionalList: l.list(validators: [l.string().required()]), +}); + +SchemaValidationResult $ListModelValidate( + Map json, +) => $ListModelSchema.validateSchema(json, fromJson: ListModel.fromJson); + +extension ListModelValidationExtension on ListModel { + SchemaValidationResult validateSelf() => + $ListModelValidate(toJson()); +} + +// ignore: constant_identifier_names +const ListModelErrorKeys = ( + stringList: "stringList", + intList: "intList", + optionalList: "optionalList", +); diff --git a/packages/luthor_generator/test/fixtures/nested_model.dart b/packages/luthor_generator/test/fixtures/nested_model.dart new file mode 100644 index 00000000..79350238 --- /dev/null +++ b/packages/luthor_generator/test/fixtures/nested_model.dart @@ -0,0 +1,19 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:luthor/luthor.dart'; +import 'simple_model.dart'; + +part 'nested_model.freezed.dart'; +part 'nested_model.g.dart'; + +@luthor +@freezed +abstract class NestedModel with _$NestedModel { + const factory NestedModel({ + required String id, + required SimpleModel user, + SimpleModel? optionalUser, + }) = _NestedModel; + + factory NestedModel.fromJson(Map json) => + _$NestedModelFromJson(json); +} diff --git a/packages/luthor_generator/test/fixtures/nested_model.freezed.dart b/packages/luthor_generator/test/fixtures/nested_model.freezed.dart new file mode 100644 index 00000000..c0cff896 --- /dev/null +++ b/packages/luthor_generator/test/fixtures/nested_model.freezed.dart @@ -0,0 +1,325 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'nested_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$NestedModel { + + String get id; SimpleModel get user; SimpleModel? get optionalUser; +/// Create a copy of NestedModel +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$NestedModelCopyWith get copyWith => _$NestedModelCopyWithImpl(this as NestedModel, _$identity); + + /// Serializes this NestedModel to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is NestedModel&&(identical(other.id, id) || other.id == id)&&(identical(other.user, user) || other.user == user)&&(identical(other.optionalUser, optionalUser) || other.optionalUser == optionalUser)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,user,optionalUser); + +@override +String toString() { + return 'NestedModel(id: $id, user: $user, optionalUser: $optionalUser)'; +} + + +} + +/// @nodoc +abstract mixin class $NestedModelCopyWith<$Res> { + factory $NestedModelCopyWith(NestedModel value, $Res Function(NestedModel) _then) = _$NestedModelCopyWithImpl; +@useResult +$Res call({ + String id, SimpleModel user, SimpleModel? optionalUser +}); + + +$SimpleModelCopyWith<$Res> get user;$SimpleModelCopyWith<$Res>? get optionalUser; + +} +/// @nodoc +class _$NestedModelCopyWithImpl<$Res> + implements $NestedModelCopyWith<$Res> { + _$NestedModelCopyWithImpl(this._self, this._then); + + final NestedModel _self; + final $Res Function(NestedModel) _then; + +/// Create a copy of NestedModel +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? user = null,Object? optionalUser = freezed,}) { + return _then(_self.copyWith( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,user: null == user ? _self.user : user // ignore: cast_nullable_to_non_nullable +as SimpleModel,optionalUser: freezed == optionalUser ? _self.optionalUser : optionalUser // ignore: cast_nullable_to_non_nullable +as SimpleModel?, + )); +} +/// Create a copy of NestedModel +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SimpleModelCopyWith<$Res> get user { + + return $SimpleModelCopyWith<$Res>(_self.user, (value) { + return _then(_self.copyWith(user: value)); + }); +}/// Create a copy of NestedModel +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SimpleModelCopyWith<$Res>? get optionalUser { + if (_self.optionalUser == null) { + return null; + } + + return $SimpleModelCopyWith<$Res>(_self.optionalUser!, (value) { + return _then(_self.copyWith(optionalUser: value)); + }); +} +} + + +/// Adds pattern-matching-related methods to [NestedModel]. +extension NestedModelPatterns on NestedModel { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _NestedModel value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _NestedModel() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _NestedModel value) $default,){ +final _that = this; +switch (_that) { +case _NestedModel(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _NestedModel value)? $default,){ +final _that = this; +switch (_that) { +case _NestedModel() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, SimpleModel user, SimpleModel? optionalUser)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _NestedModel() when $default != null: +return $default(_that.id,_that.user,_that.optionalUser);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String id, SimpleModel user, SimpleModel? optionalUser) $default,) {final _that = this; +switch (_that) { +case _NestedModel(): +return $default(_that.id,_that.user,_that.optionalUser);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, SimpleModel user, SimpleModel? optionalUser)? $default,) {final _that = this; +switch (_that) { +case _NestedModel() when $default != null: +return $default(_that.id,_that.user,_that.optionalUser);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _NestedModel implements NestedModel { + const _NestedModel({required this.id, required this.user, this.optionalUser}); + factory _NestedModel.fromJson(Map json) => _$NestedModelFromJson(json); + +@override final String id; +@override final SimpleModel user; +@override final SimpleModel? optionalUser; + +/// Create a copy of NestedModel +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$NestedModelCopyWith<_NestedModel> get copyWith => __$NestedModelCopyWithImpl<_NestedModel>(this, _$identity); + +@override +Map toJson() { + return _$NestedModelToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _NestedModel&&(identical(other.id, id) || other.id == id)&&(identical(other.user, user) || other.user == user)&&(identical(other.optionalUser, optionalUser) || other.optionalUser == optionalUser)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,user,optionalUser); + +@override +String toString() { + return 'NestedModel(id: $id, user: $user, optionalUser: $optionalUser)'; +} + + +} + +/// @nodoc +abstract mixin class _$NestedModelCopyWith<$Res> implements $NestedModelCopyWith<$Res> { + factory _$NestedModelCopyWith(_NestedModel value, $Res Function(_NestedModel) _then) = __$NestedModelCopyWithImpl; +@override @useResult +$Res call({ + String id, SimpleModel user, SimpleModel? optionalUser +}); + + +@override $SimpleModelCopyWith<$Res> get user;@override $SimpleModelCopyWith<$Res>? get optionalUser; + +} +/// @nodoc +class __$NestedModelCopyWithImpl<$Res> + implements _$NestedModelCopyWith<$Res> { + __$NestedModelCopyWithImpl(this._self, this._then); + + final _NestedModel _self; + final $Res Function(_NestedModel) _then; + +/// Create a copy of NestedModel +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? user = null,Object? optionalUser = freezed,}) { + return _then(_NestedModel( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,user: null == user ? _self.user : user // ignore: cast_nullable_to_non_nullable +as SimpleModel,optionalUser: freezed == optionalUser ? _self.optionalUser : optionalUser // ignore: cast_nullable_to_non_nullable +as SimpleModel?, + )); +} + +/// Create a copy of NestedModel +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SimpleModelCopyWith<$Res> get user { + + return $SimpleModelCopyWith<$Res>(_self.user, (value) { + return _then(_self.copyWith(user: value)); + }); +}/// Create a copy of NestedModel +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SimpleModelCopyWith<$Res>? get optionalUser { + if (_self.optionalUser == null) { + return null; + } + + return $SimpleModelCopyWith<$Res>(_self.optionalUser!, (value) { + return _then(_self.copyWith(optionalUser: value)); + }); +} +} + +// dart format on diff --git a/packages/luthor_generator/test/fixtures/nested_model.g.dart b/packages/luthor_generator/test/fixtures/nested_model.g.dart new file mode 100644 index 00000000..d222f7aa --- /dev/null +++ b/packages/luthor_generator/test/fixtures/nested_model.g.dart @@ -0,0 +1,67 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'nested_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_NestedModel _$NestedModelFromJson(Map json) => _NestedModel( + id: json['id'] as String, + user: SimpleModel.fromJson(json['user'] as Map), + optionalUser: json['optionalUser'] == null + ? null + : SimpleModel.fromJson(json['optionalUser'] as Map), +); + +Map _$NestedModelToJson(_NestedModel instance) => + { + 'id': instance.id, + 'user': instance.user, + 'optionalUser': instance.optionalUser, + }; + +// ************************************************************************** +// LuthorGenerator +// ************************************************************************** + +// ignore: constant_identifier_names +const NestedModelSchemaKeys = ( + id: "id", + user: "user", + optionalUser: "optionalUser", +); + +Validator $NestedModelSchema = l.withName('NestedModel').schema({ + NestedModelSchemaKeys.id: l.string().required(), + NestedModelSchemaKeys.user: $SimpleModelSchema.required(), + NestedModelSchemaKeys.optionalUser: $SimpleModelSchema, +}); + +SchemaValidationResult $NestedModelValidate( + Map json, +) => $NestedModelSchema.validateSchema(json, fromJson: NestedModel.fromJson); + +extension NestedModelValidationExtension on NestedModel { + SchemaValidationResult validateSelf() => + $NestedModelValidate(toJson()); +} + +// ignore: constant_identifier_names +const NestedModelErrorKeys = ( + id: "id", + user: ( + name: "user.name", + age: "user.age", + isActive: "user.isActive", + score: "user.score", + optionalField: "user.optionalField", + ), + optionalUser: ( + name: "optionalUser.name", + age: "optionalUser.age", + isActive: "optionalUser.isActive", + score: "optionalUser.score", + optionalField: "optionalUser.optionalField", + ), +); diff --git a/packages/luthor_generator/test/fixtures/simple_model.dart b/packages/luthor_generator/test/fixtures/simple_model.dart new file mode 100644 index 00000000..1372b687 --- /dev/null +++ b/packages/luthor_generator/test/fixtures/simple_model.dart @@ -0,0 +1,20 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:luthor/luthor.dart'; + +part 'simple_model.freezed.dart'; +part 'simple_model.g.dart'; + +@luthor +@freezed +abstract class SimpleModel with _$SimpleModel { + const factory SimpleModel({ + required String name, + required int age, + required bool isActive, + required double score, + String? optionalField, + }) = _SimpleModel; + + factory SimpleModel.fromJson(Map json) => + _$SimpleModelFromJson(json); +} diff --git a/packages/luthor_generator/test/fixtures/simple_model.freezed.dart b/packages/luthor_generator/test/fixtures/simple_model.freezed.dart new file mode 100644 index 00000000..44afbad4 --- /dev/null +++ b/packages/luthor_generator/test/fixtures/simple_model.freezed.dart @@ -0,0 +1,289 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'simple_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$SimpleModel { + + String get name; int get age; bool get isActive; double get score; String? get optionalField; +/// Create a copy of SimpleModel +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$SimpleModelCopyWith get copyWith => _$SimpleModelCopyWithImpl(this as SimpleModel, _$identity); + + /// Serializes this SimpleModel to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SimpleModel&&(identical(other.name, name) || other.name == name)&&(identical(other.age, age) || other.age == age)&&(identical(other.isActive, isActive) || other.isActive == isActive)&&(identical(other.score, score) || other.score == score)&&(identical(other.optionalField, optionalField) || other.optionalField == optionalField)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,name,age,isActive,score,optionalField); + +@override +String toString() { + return 'SimpleModel(name: $name, age: $age, isActive: $isActive, score: $score, optionalField: $optionalField)'; +} + + +} + +/// @nodoc +abstract mixin class $SimpleModelCopyWith<$Res> { + factory $SimpleModelCopyWith(SimpleModel value, $Res Function(SimpleModel) _then) = _$SimpleModelCopyWithImpl; +@useResult +$Res call({ + String name, int age, bool isActive, double score, String? optionalField +}); + + + + +} +/// @nodoc +class _$SimpleModelCopyWithImpl<$Res> + implements $SimpleModelCopyWith<$Res> { + _$SimpleModelCopyWithImpl(this._self, this._then); + + final SimpleModel _self; + final $Res Function(SimpleModel) _then; + +/// Create a copy of SimpleModel +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? age = null,Object? isActive = null,Object? score = null,Object? optionalField = freezed,}) { + return _then(_self.copyWith( +name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,age: null == age ? _self.age : age // ignore: cast_nullable_to_non_nullable +as int,isActive: null == isActive ? _self.isActive : isActive // ignore: cast_nullable_to_non_nullable +as bool,score: null == score ? _self.score : score // ignore: cast_nullable_to_non_nullable +as double,optionalField: freezed == optionalField ? _self.optionalField : optionalField // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [SimpleModel]. +extension SimpleModelPatterns on SimpleModel { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _SimpleModel value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _SimpleModel() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _SimpleModel value) $default,){ +final _that = this; +switch (_that) { +case _SimpleModel(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _SimpleModel value)? $default,){ +final _that = this; +switch (_that) { +case _SimpleModel() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String name, int age, bool isActive, double score, String? optionalField)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _SimpleModel() when $default != null: +return $default(_that.name,_that.age,_that.isActive,_that.score,_that.optionalField);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String name, int age, bool isActive, double score, String? optionalField) $default,) {final _that = this; +switch (_that) { +case _SimpleModel(): +return $default(_that.name,_that.age,_that.isActive,_that.score,_that.optionalField);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String name, int age, bool isActive, double score, String? optionalField)? $default,) {final _that = this; +switch (_that) { +case _SimpleModel() when $default != null: +return $default(_that.name,_that.age,_that.isActive,_that.score,_that.optionalField);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _SimpleModel implements SimpleModel { + const _SimpleModel({required this.name, required this.age, required this.isActive, required this.score, this.optionalField}); + factory _SimpleModel.fromJson(Map json) => _$SimpleModelFromJson(json); + +@override final String name; +@override final int age; +@override final bool isActive; +@override final double score; +@override final String? optionalField; + +/// Create a copy of SimpleModel +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$SimpleModelCopyWith<_SimpleModel> get copyWith => __$SimpleModelCopyWithImpl<_SimpleModel>(this, _$identity); + +@override +Map toJson() { + return _$SimpleModelToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _SimpleModel&&(identical(other.name, name) || other.name == name)&&(identical(other.age, age) || other.age == age)&&(identical(other.isActive, isActive) || other.isActive == isActive)&&(identical(other.score, score) || other.score == score)&&(identical(other.optionalField, optionalField) || other.optionalField == optionalField)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,name,age,isActive,score,optionalField); + +@override +String toString() { + return 'SimpleModel(name: $name, age: $age, isActive: $isActive, score: $score, optionalField: $optionalField)'; +} + + +} + +/// @nodoc +abstract mixin class _$SimpleModelCopyWith<$Res> implements $SimpleModelCopyWith<$Res> { + factory _$SimpleModelCopyWith(_SimpleModel value, $Res Function(_SimpleModel) _then) = __$SimpleModelCopyWithImpl; +@override @useResult +$Res call({ + String name, int age, bool isActive, double score, String? optionalField +}); + + + + +} +/// @nodoc +class __$SimpleModelCopyWithImpl<$Res> + implements _$SimpleModelCopyWith<$Res> { + __$SimpleModelCopyWithImpl(this._self, this._then); + + final _SimpleModel _self; + final $Res Function(_SimpleModel) _then; + +/// Create a copy of SimpleModel +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? age = null,Object? isActive = null,Object? score = null,Object? optionalField = freezed,}) { + return _then(_SimpleModel( +name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,age: null == age ? _self.age : age // ignore: cast_nullable_to_non_nullable +as int,isActive: null == isActive ? _self.isActive : isActive // ignore: cast_nullable_to_non_nullable +as bool,score: null == score ? _self.score : score // ignore: cast_nullable_to_non_nullable +as double,optionalField: freezed == optionalField ? _self.optionalField : optionalField // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + + +} + +// dart format on diff --git a/packages/luthor_generator/test/fixtures/simple_model.g.dart b/packages/luthor_generator/test/fixtures/simple_model.g.dart new file mode 100644 index 00000000..760229cb --- /dev/null +++ b/packages/luthor_generator/test/fixtures/simple_model.g.dart @@ -0,0 +1,63 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'simple_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_SimpleModel _$SimpleModelFromJson(Map json) => _SimpleModel( + name: json['name'] as String, + age: (json['age'] as num).toInt(), + isActive: json['isActive'] as bool, + score: (json['score'] as num).toDouble(), + optionalField: json['optionalField'] as String?, +); + +Map _$SimpleModelToJson(_SimpleModel instance) => + { + 'name': instance.name, + 'age': instance.age, + 'isActive': instance.isActive, + 'score': instance.score, + 'optionalField': instance.optionalField, + }; + +// ************************************************************************** +// LuthorGenerator +// ************************************************************************** + +// ignore: constant_identifier_names +const SimpleModelSchemaKeys = ( + name: "name", + age: "age", + isActive: "isActive", + score: "score", + optionalField: "optionalField", +); + +Validator $SimpleModelSchema = l.withName('SimpleModel').schema({ + SimpleModelSchemaKeys.name: l.string().required(), + SimpleModelSchemaKeys.age: l.int().required(), + SimpleModelSchemaKeys.isActive: l.boolean().required(), + SimpleModelSchemaKeys.score: l.double().required(), + SimpleModelSchemaKeys.optionalField: l.string(), +}); + +SchemaValidationResult $SimpleModelValidate( + Map json, +) => $SimpleModelSchema.validateSchema(json, fromJson: SimpleModel.fromJson); + +extension SimpleModelValidationExtension on SimpleModel { + SchemaValidationResult validateSelf() => + $SimpleModelValidate(toJson()); +} + +// ignore: constant_identifier_names +const SimpleModelErrorKeys = ( + name: "name", + age: "age", + isActive: "isActive", + score: "score", + optionalField: "optionalField", +); diff --git a/packages/luthor_generator/test/generator_test.dart b/packages/luthor_generator/test/generator_test.dart new file mode 100644 index 00000000..dc85f63e --- /dev/null +++ b/packages/luthor_generator/test/generator_test.dart @@ -0,0 +1,492 @@ +import 'package:luthor/luthor.dart'; +import 'package:test/test.dart'; + +import 'fixtures/annotated_model.dart'; +import 'fixtures/json_key_model.dart'; +import 'fixtures/list_model.dart'; +import 'fixtures/nested_model.dart'; +import 'fixtures/simple_model.dart'; + +void main() { + group('Schema Generation Tests', () { + test('should generate schema variable with correct name', () { + expect($SimpleModelSchema, isA()); + }); + + test('should generate SchemaKeys with correct field names', () { + expect(SimpleModelSchemaKeys.name, equals('name')); + expect(SimpleModelSchemaKeys.age, equals('age')); + expect(SimpleModelSchemaKeys.isActive, equals('isActive')); + expect(SimpleModelSchemaKeys.score, equals('score')); + expect(SimpleModelSchemaKeys.optionalField, equals('optionalField')); + }); + + test('should generate ErrorKeys with correct field names', () { + expect(SimpleModelErrorKeys.name, equals('name')); + expect(SimpleModelErrorKeys.age, equals('age')); + expect(SimpleModelErrorKeys.isActive, equals('isActive')); + expect(SimpleModelErrorKeys.score, equals('score')); + }); + + test('should generate validate function with correct name', () { + final result = $SimpleModelValidate({}); + expect(result, isA>()); + }); + + test('should generate validateSelf extension method', () { + final validData = { + 'name': 'Test Name', + 'age': 25, + 'isActive': true, + 'score': 85.5, + }; + + switch ($SimpleModelValidate(validData)) { + case SchemaValidationSuccess(data: final model): + final selfResult = model.validateSelf(); + expect(selfResult, isA>()); + case SchemaValidationError(errors: final errors): + fail('Should not have validation errors: $errors'); + } + }); + }); + + group('Basic Type Validation Tests', () { + test('should validate required string field', () { + final result = $SimpleModelValidate({}); + + switch (result) { + case SchemaValidationSuccess(data: _): + fail('Should have validation errors for missing required fields'); + case SchemaValidationError(errors: final errors): + expect(errors.keys, contains('name')); + } + }); + + test('should validate required int field', () { + final result = $SimpleModelValidate({}); + + switch (result) { + case SchemaValidationSuccess(data: _): + fail('Should have validation errors for missing required fields'); + case SchemaValidationError(errors: final errors): + expect(errors.keys, contains('age')); + } + }); + + test('should validate required bool field', () { + final result = $SimpleModelValidate({}); + + switch (result) { + case SchemaValidationSuccess(data: _): + fail('Should have validation errors for missing required fields'); + case SchemaValidationError(errors: final errors): + expect(errors.keys, contains('isActive')); + } + }); + + test('should validate required double field', () { + final result = $SimpleModelValidate({}); + + switch (result) { + case SchemaValidationSuccess(data: _): + fail('Should have validation errors for missing required fields'); + case SchemaValidationError(errors: final errors): + expect(errors.keys, contains('score')); + } + }); + + test('should allow optional field to be missing', () { + final data = {'name': 'Test', 'age': 25, 'isActive': true, 'score': 85.5}; + + final result = $SimpleModelValidate(data); + + switch (result) { + case SchemaValidationSuccess(data: _): + expect(result.data, isA()); + case SchemaValidationError(errors: _): + fail('Should not have errors for missing optional field'); + } + }); + + test('should validate correct types', () { + final validData = { + 'name': 'Test Name', + 'age': 25, + 'isActive': true, + 'score': 85.5, + }; + + final result = $SimpleModelValidate(validData); + + switch (result) { + case SchemaValidationSuccess(data: final model): + expect(model.name, equals('Test Name')); + expect(model.age, equals(25)); + expect(model.isActive, isTrue); + expect(model.score, equals(85.5)); + case SchemaValidationError(errors: final errors): + fail('Should not have validation errors: $errors'); + } + }); + + test('should reject incorrect types', () { + final invalidData = { + 'name': 123, // should be string + 'age': 'twenty-five', // should be int + 'isActive': 'yes', // should be bool + 'score': 'high', // should be double + }; + + final result = $SimpleModelValidate(invalidData); + + switch (result) { + case SchemaValidationSuccess(data: _): + fail('Should have validation errors for wrong types'); + case SchemaValidationError(errors: final errors): + expect(errors.keys, contains('name')); + expect(errors.keys, contains('age')); + expect(errors.keys, contains('isActive')); + expect(errors.keys, contains('score')); + } + }); + }); + + group('Annotation Validation Tests', () { + test('should validate @IsEmail annotation', () { + final data = { + 'email': 'invalid-email', + 'password': 'password123', + 'age': 25, + 'score': 50.0, + 'uuid': 'f47ac10b-58cc-4372-a567-0e02b2c3d479', + }; + + final result = $AnnotatedModelValidate(data); + + switch (result) { + case SchemaValidationSuccess(data: _): + fail('Should have validation error for invalid email'); + case SchemaValidationError(errors: final errors): + expect(errors.keys, contains('email')); + } + }); + + test('should validate @HasMin and @HasMax for string', () { + final dataMin = { + 'email': 'test@example.com', + 'password': 'short', // min is 8 + 'age': 25, + 'score': 50.0, + 'uuid': 'f47ac10b-58cc-4372-a567-0e02b2c3d479', + }; + + final result = $AnnotatedModelValidate(dataMin); + + switch (result) { + case SchemaValidationSuccess(data: _): + fail('Should have validation error for string too short'); + case SchemaValidationError(errors: final errors): + expect(errors.keys, contains('password')); + } + }); + + test('should validate @HasMin and @HasMax for int', () { + final dataOutOfRange = { + 'email': 'test@example.com', + 'password': 'password123', + 'age': 15, // min is 18 + 'score': 50.0, + 'uuid': 'f47ac10b-58cc-4372-a567-0e02b2c3d479', + }; + + final result = $AnnotatedModelValidate(dataOutOfRange); + + switch (result) { + case SchemaValidationSuccess(data: _): + fail('Should have validation error for int out of range'); + case SchemaValidationError(errors: final errors): + expect(errors.keys, contains('age')); + } + }); + + test('should validate @HasMinDouble and @HasMaxDouble', () { + final dataOutOfRange = { + 'email': 'test@example.com', + 'password': 'password123', + 'age': 25, + 'score': 150.0, // max is 100.0 + 'uuid': 'f47ac10b-58cc-4372-a567-0e02b2c3d479', + }; + + final result = $AnnotatedModelValidate(dataOutOfRange); + + switch (result) { + case SchemaValidationSuccess(data: _): + fail('Should have validation error for double out of range'); + case SchemaValidationError(errors: final errors): + expect(errors.keys, contains('score')); + } + }); + + test('should validate @StartsWith annotation', () { + final data = { + 'email': 'test@example.com', + 'password': 'password123', + 'age': 25, + 'score': 50.0, + 'uuid': 'f47ac10b-58cc-4372-a567-0e02b2c3d479', + 'website': 'http://example.com', // should start with https:// + }; + + final result = $AnnotatedModelValidate(data); + + switch (result) { + case SchemaValidationSuccess(data: _): + fail( + 'Should have validation error for string not starting with https://', + ); + case SchemaValidationError(errors: final errors): + expect(errors.keys, contains('website')); + } + }); + + test('should validate @EndsWith annotation', () { + final data = { + 'email': 'test@example.com', + 'password': 'password123', + 'age': 25, + 'score': 50.0, + 'uuid': 'f47ac10b-58cc-4372-a567-0e02b2c3d479', + 'domain': 'example.org', // should end with .com + }; + + final result = $AnnotatedModelValidate(data); + + switch (result) { + case SchemaValidationSuccess(data: _): + fail('Should have validation error for string not ending with .com'); + case SchemaValidationError(errors: final errors): + expect(errors.keys, contains('domain')); + } + }); + + test('should validate @Contains annotation', () { + final data = { + 'email': 'test@example.com', + 'password': 'password123', + 'age': 25, + 'score': 50.0, + 'uuid': 'f47ac10b-58cc-4372-a567-0e02b2c3d479', + 'testField': 'production', // should contain 'test' + }; + + final result = $AnnotatedModelValidate(data); + + switch (result) { + case SchemaValidationSuccess(data: _): + fail('Should have validation error for string not containing test'); + case SchemaValidationError(errors: final errors): + expect(errors.keys, contains('testField')); + } + }); + + test('should validate @IsUuid annotation', () { + final data = { + 'email': 'test@example.com', + 'password': 'password123', + 'age': 25, + 'score': 50.0, + 'uuid': 'not-a-uuid', + }; + + final result = $AnnotatedModelValidate(data); + + switch (result) { + case SchemaValidationSuccess(data: _): + fail('Should have validation error for invalid UUID'); + case SchemaValidationError(errors: final errors): + expect(errors.keys, contains('uuid')); + } + }); + + test('should pass with all valid annotations', () { + final validData = { + 'email': 'test@example.com', + 'password': 'password123', + 'age': 25, + 'score': 50.0, + 'uuid': 'f47ac10b-58cc-4372-a567-0e02b2c3d479', + 'website': 'https://example.com', + 'domain': 'example.com', + 'testField': 'test data', + }; + + final result = $AnnotatedModelValidate(validData); + + switch (result) { + case SchemaValidationSuccess(data: final model): + expect(model.email, equals('test@example.com')); + expect(model.password, equals('password123')); + case SchemaValidationError(errors: final errors): + fail('Should not have validation errors: $errors'); + } + }); + }); + + group('@JsonKey Tests', () { + test('should respect @JsonKey annotation in SchemaKeys', () { + expect(JsonKeyModelSchemaKeys.userName, equals('user_name')); + expect(JsonKeyModelSchemaKeys.userEmail, equals('user_email')); + expect(JsonKeyModelSchemaKeys.normalField, equals('normalField')); + }); + + test('should respect @JsonKey annotation in ErrorKeys', () { + expect(JsonKeyModelErrorKeys.userName, equals('user_name')); + expect(JsonKeyModelErrorKeys.userEmail, equals('user_email')); + expect(JsonKeyModelErrorKeys.normalField, equals('normalField')); + }); + + test('should validate using JSON key names', () { + final data = { + 'user_name': 'testuser', + 'user_email': 'test@example.com', + 'normalField': 'value', + }; + + final result = $JsonKeyModelValidate(data); + + switch (result) { + case SchemaValidationSuccess(data: final model): + expect(model.userName, equals('testuser')); + expect(model.userEmail, equals('test@example.com')); + case SchemaValidationError(errors: final errors): + fail('Should not have validation errors: $errors'); + } + }); + + test('should validate email with @JsonKey field', () { + final data = { + 'user_name': 'testuser', + 'user_email': 'invalid-email', + 'normalField': 'value', + }; + + final result = $JsonKeyModelValidate(data); + + switch (result) { + case SchemaValidationSuccess(data: _): + fail('Should have validation error for invalid email'); + case SchemaValidationError(errors: final errors): + expect(errors.keys, contains('user_email')); + } + }); + }); + + group('Nested Schema Tests', () { + test('should generate nested ErrorKeys', () { + expect(NestedModelErrorKeys.id, equals('id')); + expect(NestedModelErrorKeys.user.name, equals('user.name')); + expect(NestedModelErrorKeys.user.age, equals('user.age')); + }); + + test('should validate nested schema', () { + final validData = { + 'id': 'test-id', + 'user': { + 'name': 'John Doe', + 'age': 30, + 'isActive': true, + 'score': 95.5, + }, + }; + + final result = $NestedModelValidate(validData); + + switch (result) { + case SchemaValidationSuccess(data: final model): + expect(model.id, equals('test-id')); + expect(model.user.name, equals('John Doe')); + expect(model.user.age, equals(30)); + case SchemaValidationError(errors: final errors): + fail('Should not have validation errors: $errors'); + } + }); + + test('should validate nested schema with errors', () { + final invalidData = { + 'id': 'test-id', + 'user': { + 'name': 'John Doe', + // Missing required fields + }, + }; + + final result = $NestedModelValidate(invalidData); + + switch (result) { + case SchemaValidationSuccess(data: _): + fail('Should have validation errors for nested schema'); + case SchemaValidationError(errors: final errors): + // Nested schema errors are reported under the parent key + expect(errors.keys, contains('user')); + expect(errors['user'], isNotEmpty); + } + }); + + test('should generate auto-schema for nested SimpleModel', () { + expect($SimpleModelSchema, isA()); + }); + }); + + group('List Validation Tests', () { + test('should validate list of strings', () { + final validData = { + 'stringList': ['a', 'b', 'c'], + 'intList': [1, 2, 3], + }; + + final result = $ListModelValidate(validData); + + switch (result) { + case SchemaValidationSuccess(data: final model): + expect(model.stringList, equals(['a', 'b', 'c'])); + expect(model.intList, equals([1, 2, 3])); + case SchemaValidationError(errors: final errors): + fail('Should not have validation errors: $errors'); + } + }); + + test('should validate list of ints', () { + final invalidData = { + 'stringList': ['a', 'b'], + 'intList': ['not', 'numbers'], + }; + + final result = $ListModelValidate(invalidData); + + switch (result) { + case SchemaValidationSuccess(data: _): + fail('Should have validation error for invalid list items'); + case SchemaValidationError(errors: final errors): + expect(errors.keys, contains('intList')); + } + }); + + test('should allow optional list to be missing', () { + final data = { + 'stringList': ['a', 'b'], + 'intList': [1, 2], + }; + + final result = $ListModelValidate(data); + + switch (result) { + case SchemaValidationSuccess(data: _): + expect(result.data, isA()); + case SchemaValidationError(errors: _): + fail('Should not have errors for missing optional list'); + } + }); + }); +} diff --git a/pubspec.lock b/pubspec.lock index 04300d53..9056042d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d + sha256: f0bb5d1648339c8308cc0b9838d8456b3cfe5c91f9dc1a735b4d003269e5da9a url: "https://pub.dev" source: hosted - version: "91.0.0" + version: "88.0.0" analyzer: dependency: "direct main" description: name: analyzer - sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08 + sha256: "0b7b9c329d2879f8f05d6c05b32ee9ec025f39b077864bdb5ac9a7b63418a98f" url: "https://pub.dev" source: hosted - version: "8.4.1" + version: "8.1.1" ansicolor: dependency: transitive description: @@ -173,10 +173,10 @@ packages: dependency: transitive description: name: dart_style - sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b + sha256: c87dfe3d56f183ffe9106a18aebc6db431fc7c98c31a54b952a77f3d54a85697 url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.2" file: dependency: transitive description: @@ -578,4 +578,4 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.9.0 <4.0.0" + dart: ">=3.8.0 <4.0.0"