diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc3e0f47..db52269a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ on: jobs: test: - uses: btwld/dart-actions/.github/workflows/ci.yml@main + uses: btwld/dart-actions/.github/workflows/ci.yml@9075ce1232ec77b8747953f2ff4a349190e5a805 secrets: token: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 09a0c181..3681a303 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,8 +6,26 @@ on: - 'v*' jobs: + validate-tag: + runs-on: ubuntu-latest + outputs: + semver_ok: ${{ steps.validate-tag.outputs.semver_ok }} + steps: + - name: Validate release tag as SemVer 2.0.0 with optional pre-release/build metadata + id: validate-tag + shell: bash + run: | + TAG="${{ github.ref_name }}" + if [[ ! "$TAG" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-(0|[1-9][0-9]*|[0-9A-Za-z-]+\.)*[0-9A-Za-z-]+)?(\+(0|[1-9][0-9]*|[0-9A-Za-z-]+\.)*[0-9A-Za-z-]+)?$ ]]; then + echo "::error::Release tag '$TAG' is not SemVer 2.0.0 compatible. Use tags like v1.0.0, v1.0.0-alpha.1, or v1.0.0-beta.6+exp.sha." + exit 1 + fi + echo "semver_ok=true" >> "$GITHUB_OUTPUT" + publish: - uses: btwld/dart-actions/.github/workflows/publish.yml@main + needs: validate-tag + if: needs.validate-tag.outputs.semver_ok == 'true' + uses: btwld/dart-actions/.github/workflows/publish.yml@9075ce1232ec77b8747953f2ff4a349190e5a805 permissions: id-token: write with: diff --git a/docs/api-reference/index.mdx b/docs/api-reference/index.mdx index c0cb0467..20dde53f 100644 --- a/docs/api-reference/index.mdx +++ b/docs/api-reference/index.mdx @@ -18,7 +18,7 @@ Entry point for creating schemas. See [Schema Types](../core-concepts/schemas.md - `Ack.enumString(List values)`: Creates a string schema that only accepts specific values. - `Ack.anyOf(List schemas)`: Creates an `AnyOfSchema` for union types. - `Ack.any()`: Creates an `AnySchema` that accepts any value. -- `Ack.discriminated({required String discriminatorKey, required Map schemas})`: Creates a discriminated union schema. +- `Ack.discriminated({required String discriminatorKey, required Map>> schemas})`: Creates a discriminated union schema. ## `AckSchema` (Base Class) @@ -285,7 +285,7 @@ Schema for union types (value must match one of several schemas). Schema for polymorphic validation based on a discriminator field. -- Created using `Ack.discriminated({required String discriminatorKey, required Map schemas})` +- Created using `Ack.discriminated({required String discriminatorKey, required Map>> schemas})` ### `AnySchema` diff --git a/example/test/extension_type_to_json_test.dart b/example/test/extension_type_to_json_test.dart new file mode 100644 index 00000000..c831da7e --- /dev/null +++ b/example/test/extension_type_to_json_test.dart @@ -0,0 +1,28 @@ +import 'package:ack_example/schema_types_primitives.dart'; +import 'package:ack_example/schema_types_simple.dart'; +import 'package:test/test.dart'; + +void main() { + group('Extension type toJson', () { + test('object extension type returns map data', () { + final user = UserType.parse({'name': 'Alice', 'age': 30, 'active': true}); + + expect(user.toJson(), {'name': 'Alice', 'age': 30, 'active': true}); + expect(user.toJson(), isA>()); + }); + + test('primitive extension type returns wrapped value', () { + final password = PasswordType.parse('mySecurePassword123'); + + expect(password.toJson(), 'mySecurePassword123'); + expect(password.toJson(), isA()); + }); + + test('collection extension type returns wrapped list', () { + final tags = TagsType.parse(['dart', 'ack']); + + expect(tags.toJson(), ['dart', 'ack']); + expect(tags.toJson(), isA>()); + }); + }); +} diff --git a/packages/ack/lib/src/schemas/transformed_schema.dart b/packages/ack/lib/src/schemas/transformed_schema.dart index 09ada782..8eca6947 100644 --- a/packages/ack/lib/src/schemas/transformed_schema.dart +++ b/packages/ack/lib/src/schemas/transformed_schema.dart @@ -60,36 +60,24 @@ class TransformedSchema Object? inputValue, SchemaContext context, ) { - // Handle TransformedSchema's own defaultValue for null input. - // Clone the default to prevent mutation of shared state. - // This must happen BEFORE delegating to wrapped schema, because the wrapped - // schema might not accept null (e.g., non-nullable StringSchema). - // - // NOTE: cloneDefault() returns List or Map for - // collections, which cannot be safely cast to parameterized OutputType like - // List. We use runtime type checking: if the clone is type-compatible, - // use it; otherwise fall back to the original (accepts mutation risk for - // parameterized collection defaults, but avoids runtime TypeError). + // Handle defaults before delegation, since wrapped schemas may reject null. + // If cloning loses generic type information, fall back to the original value. if (inputValue == null && defaultValue != null) { final cloned = cloneDefault(defaultValue!); final safeDefault = (cloned is OutputType) ? cloned : defaultValue!; return applyConstraintsAndRefinements(safeDefault, context); } - // For non-null input OR null input without default: - // Delegate to underlying schema (handles type conversion, null validation, constraints) - // The inner schema determines if null is valid based on its own isNullable setting. + // Delegate validation/parsing to the wrapped schema for all other cases. final originalResult = schema.parseAndValidate(inputValue, context); if (originalResult.isFail) { return SchemaResult.fail(originalResult.getError()); } - // Transform the validated value (may be null if underlying schema is nullable) final validatedValue = originalResult.getOrNull(); try { final transformedValue = transformer(validatedValue); - // Apply TransformedSchema's own constraints and refinements return applyConstraintsAndRefinements(transformedValue, context); } catch (e, st) { return SchemaResult.fail( diff --git a/packages/ack_annotations/CHANGELOG.md b/packages/ack_annotations/CHANGELOG.md index 2711b522..910bdbae 100644 --- a/packages/ack_annotations/CHANGELOG.md +++ b/packages/ack_annotations/CHANGELOG.md @@ -4,6 +4,7 @@ * **AckType**: Refined annotation parameters and improved type handling (#50). * **AckField**: Improved field annotation correctness (#50). +* **Breaking**: `AckField.required` was replaced by `requiredMode` (`AckFieldRequiredMode.auto|required|optional`). Migrate `@AckField(required: true)` to `@AckField(requiredMode: AckFieldRequiredMode.required)` and `required: false` to `requiredMode: AckFieldRequiredMode.optional`. ## 1.0.0-beta.5 (2026-01-14) diff --git a/packages/ack_annotations/README.md b/packages/ack_annotations/README.md index 2a3fc7b3..b3cdadcf 100644 --- a/packages/ack_annotations/README.md +++ b/packages/ack_annotations/README.md @@ -10,10 +10,10 @@ Add to your `pubspec.yaml` (check [pub.dev](https://pub.dev/packages/ack_annotat ```yaml dependencies: - ack_annotations: ^1.0.0 + ack_annotations: ^1.0.0-beta.6 dev_dependencies: - ack_generator: ^1.0.0 + ack_generator: ^1.0.0-beta.6 build_runner: ^2.4.0 ``` diff --git a/packages/ack_generator/README.md b/packages/ack_generator/README.md index 7ec0c5e5..537cf15b 100644 --- a/packages/ack_generator/README.md +++ b/packages/ack_generator/README.md @@ -19,11 +19,11 @@ Add the following dependencies to your `pubspec.yaml` (check [pub.dev](https://p ```yaml dependencies: - ack: ^1.0.0 - ack_annotations: ^1.0.0 + ack: ^1.0.0-beta.6 + ack_annotations: ^1.0.0-beta.6 dev_dependencies: - ack_generator: ^1.0.0 + ack_generator: ^1.0.0-beta.6 build_runner: ^2.4.0 ``` diff --git a/packages/ack_generator/lib/src/analyzer/field_analyzer.dart b/packages/ack_generator/lib/src/analyzer/field_analyzer.dart index 31a0f04a..5d00a477 100644 --- a/packages/ack_generator/lib/src/analyzer/field_analyzer.dart +++ b/packages/ack_generator/lib/src/analyzer/field_analyzer.dart @@ -69,14 +69,11 @@ class FieldAnalyzer { } // Tri-state mode is authoritative. - switch (_getRequiredMode(annotation)) { - case AckFieldRequiredMode.required: - return true; - case AckFieldRequiredMode.optional: - return false; - case AckFieldRequiredMode.auto: - return _inferRequiredFromField(field); - } + return switch (_getRequiredMode(annotation)) { + AckFieldRequiredMode.required => true, + AckFieldRequiredMode.optional => false, + AckFieldRequiredMode.auto => _inferRequiredFromField(field), + }; } bool _inferRequiredFromField(FieldElement2 field) { @@ -89,16 +86,12 @@ class FieldAnalyzer { .getField('requiredMode') ?.getField('index') ?.toIntValue(); - switch (modeIndex) { - case 0: - return AckFieldRequiredMode.auto; - case 1: - return AckFieldRequiredMode.required; - case 2: - return AckFieldRequiredMode.optional; - default: - return AckFieldRequiredMode.auto; - } + return switch (modeIndex) { + 0 => AckFieldRequiredMode.auto, + 1 => AckFieldRequiredMode.required, + 2 => AckFieldRequiredMode.optional, + _ => AckFieldRequiredMode.auto, + }; } List _extractConstraints( diff --git a/packages/ack_generator/lib/src/analyzer/schema_ast_analyzer.dart b/packages/ack_generator/lib/src/analyzer/schema_ast_analyzer.dart index e659f088..695bb28f 100644 --- a/packages/ack_generator/lib/src/analyzer/schema_ast_analyzer.dart +++ b/packages/ack_generator/lib/src/analyzer/schema_ast_analyzer.dart @@ -533,6 +533,18 @@ class SchemaAstAnalyzer { baseInvocation, element, ); + final schemaMethod = baseInvocation.methodName.name; + + String? displayTypeOverride; + String? collectionElementDisplayTypeOverride; + + if (schemaMethod == 'enumValues') { + displayTypeOverride = _extractEnumTypeNameFromInvocation(baseInvocation); + } else if (schemaMethod == 'list') { + collectionElementDisplayTypeOverride = _extractListEnumElementTypeName( + baseInvocation, + ); + } return FieldInfo( name: fieldName, @@ -542,6 +554,9 @@ class SchemaAstAnalyzer { isNullable: isNullable, constraints: [], listElementSchemaRef: listElementSchemaRef, + displayTypeOverride: displayTypeOverride, + collectionElementDisplayTypeOverride: + collectionElementDisplayTypeOverride, ); } @@ -583,6 +598,24 @@ class SchemaAstAnalyzer { ), null, ); + case 'enumString': + case 'literal': + return (typeProvider.stringType, null); + case 'enumValues': + final resolvedType = _resolveEnumValuesType( + invocation, + library: library, + ); + if (resolvedType != null) { + return (resolvedType, null); + } + // Fallback to `dynamic` if the enum type can't be resolved. + // This avoids incorrectly assuming `String` when EnumSchema.parse() + // returns the enum value type T. + _log.warning( + 'Could not resolve enum type for Ack.enumValues(); falling back to dynamic.', + ); + return (typeProvider.dynamicType, null); default: throw InvalidGenerationSourceError( 'Unsupported schema method: Ack.$schemaMethod()', @@ -591,6 +624,329 @@ class SchemaAstAnalyzer { } } + /// Extracts the enum type name from an `Ack.enumValues(...)` invocation. + /// + /// Prefers source text only when it contains a qualifier + /// (e.g., `alias.UserRole`) so import prefixes are preserved in generated + /// part files. + /// + /// For non-qualified names, prefers resolved static types to avoid + /// incorrectly treating arbitrary `.values` receivers as enum type names + /// (for example, `holder.values` should resolve to the list element type). + String? _extractEnumTypeNameFromInvocation(MethodInvocation invocation) { + final sourceTypeName = _extractEnumTypeNameFromSource(invocation); + if (sourceTypeName != null && sourceTypeName.contains('.')) { + return sourceTypeName; + } + + final resolvedType = _resolveEnumValuesType(invocation); + if (resolvedType != null) { + return resolvedType.getDisplayString(withNullability: false); + } + + return sourceTypeName; + } + + String? _extractEnumTypeNameFromSource(MethodInvocation invocation) { + // From type argument: Ack.enumValues(...) or Ack.enumValues(...) + final typeArgs = invocation.typeArguments?.arguments; + if (typeArgs != null && typeArgs.isNotEmpty) { + return typeArgs.first.toSource(); + } + + // From argument pattern: Ack.enumValues(UserRole.values) / Ack.enumValues(alias.UserRole.values) + final args = invocation.argumentList.arguments; + if (args.isNotEmpty) { + final firstArg = args.first; + if (firstArg is PrefixedIdentifier && + firstArg.identifier.name == 'values') { + final targetSource = firstArg.prefix.toSource(); + if (_looksLikeTypeReference(targetSource)) { + return targetSource; + } + } + if (firstArg is PropertyAccess && + firstArg.propertyName.name == 'values') { + final targetSource = firstArg.target?.toSource(); + if (targetSource != null && _looksLikeTypeReference(targetSource)) { + return targetSource; + } + } + } + + return null; + } + + bool _looksLikeTypeReference(String source) { + final trimmed = source.trim(); + if (trimmed.isEmpty) return false; + + final identifier = trimmed.split('.').last; + if (identifier.isEmpty) return false; + + final firstCodeUnit = identifier.codeUnitAt(0); + const uppercaseA = 65; + const uppercaseZ = 90; + const underscore = 95; + return (firstCodeUnit >= uppercaseA && firstCodeUnit <= uppercaseZ) || + firstCodeUnit == underscore; + } + + String? _extractListEnumElementTypeName(MethodInvocation listInvocation) { + final args = listInvocation.argumentList.arguments; + if (args.isEmpty) return null; + + final ref = _resolveListElementRef(args.first); + final elementSchema = ref.ackBase; + if (elementSchema == null || + elementSchema.methodName.name != 'enumValues') { + return null; + } + + return _extractEnumTypeNameFromInvocation(elementSchema); + } + + /// Resolves enum type `T` from an `Ack.enumValues(...)` invocation. + /// + /// Resolution strategy (in order): + /// 1. Explicit type argument's resolved type (`Ack.enumValues(...)`) + /// 2. Invocation static type argument (`EnumSchema`) + /// 3. First argument static type (`List` from `T.values`) + /// 4. Source name lookup in the library/import scope + DartType? _resolveEnumValuesType( + MethodInvocation invocation, { + LibraryElement2? library, + }) { + final typeArgs = invocation.typeArguments?.arguments; + if (typeArgs != null && typeArgs.isNotEmpty) { + final explicitType = typeArgs.first.type; + if (explicitType is InterfaceType) { + return explicitType; + } + } + + final invocationType = invocation.staticType; + if (invocationType is InterfaceType && + invocationType.typeArguments.isNotEmpty) { + final schemaTypeArg = invocationType.typeArguments.first; + if (schemaTypeArg is InterfaceType) { + return schemaTypeArg; + } + } + + final args = invocation.argumentList.arguments; + if (args.isNotEmpty) { + final resolvedFromArgument = _resolveEnumValuesTypeFromArgument( + args.first, + library: library, + ); + if (resolvedFromArgument != null) { + return resolvedFromArgument; + } + } + + if (library != null) { + final enumTypeName = _extractEnumTypeNameFromSource(invocation); + if (enumTypeName != null) { + final resolvedByName = _resolveTypeByName(enumTypeName, library); + if (resolvedByName != null) { + return resolvedByName; + } + } + } + + return null; + } + + DartType? _resolveEnumValuesTypeFromArgument( + Expression argument, { + LibraryElement2? library, + }) { + final enumFromStaticType = _extractEnumTypeFromCandidate( + argument.staticType, + ); + if (enumFromStaticType != null) { + return enumFromStaticType; + } + + if (library == null) { + return null; + } + + final resolvedExpressionType = _resolveExpressionType(argument, library); + return _extractEnumTypeFromCandidate(resolvedExpressionType); + } + + DartType? _extractEnumTypeFromCandidate(DartType? candidate) { + if (candidate is! InterfaceType) { + return null; + } + + if (candidate.element3 is EnumElement2) { + return candidate; + } + + if (candidate.isDartCoreList && candidate.typeArguments.isNotEmpty) { + final elementType = candidate.typeArguments.first; + if (elementType is InterfaceType && + elementType.element3 is EnumElement2) { + return elementType; + } + } + + return null; + } + + DartType? _resolveExpressionType( + Expression expression, + LibraryElement2 library, + ) { + final staticType = expression.staticType; + if (staticType != null && staticType is! DynamicType) { + return staticType; + } + + if (expression is SimpleIdentifier) { + final variableType = _schemaVarsByName(library)[expression.name]?.type; + if (variableType != null) { + return variableType; + } + + final getterType = _schemaGettersByName( + library, + )[expression.name]?.returnType; + if (getterType != null) { + return getterType; + } + + return _resolveTypeByName(expression.name, library); + } + + if (expression is PrefixedIdentifier) { + final targetType = _resolveExpressionType(expression.prefix, library); + if (targetType is InterfaceType) { + final memberType = _resolveClassMemberType( + targetType: targetType, + memberName: expression.identifier.name, + library: library, + ); + if (memberType != null) { + return memberType; + } + } + + return _resolveTypeByName(expression.toSource(), library); + } + + if (expression is PropertyAccess) { + final target = expression.target; + if (target != null) { + final targetType = _resolveExpressionType(target, library); + if (targetType is InterfaceType) { + final memberType = _resolveClassMemberType( + targetType: targetType, + memberName: expression.propertyName.name, + library: library, + ); + if (memberType != null) { + return memberType; + } + } + } + } + + return null; + } + + DartType? _resolveClassMemberType({ + required InterfaceType targetType, + required String memberName, + required LibraryElement2 library, + }) { + final className = targetType.element3?.name3; + if (className == null) return null; + + final classElement = _classesByName(library)[className]; + if (classElement == null) return null; + + final allFields = [ + ...classElement.fields2, + ...classElement.allSupertypes.expand((type) => type.element3.fields2), + ]; + + final field = allFields.cast().firstWhere( + (current) => current?.name3 == memberName, + orElse: () => null, + ); + if (field != null) { + return field.type; + } + + final allGetters = [ + ...classElement.getters2, + ...classElement.allSupertypes.expand((type) => type.element3.getters2), + ]; + + final getter = allGetters.cast().firstWhere( + (current) => current?.name3 == memberName, + orElse: () => null, + ); + return getter?.returnType; + } + + DartType? _resolveTypeByName(String typeName, LibraryElement2 library) { + final normalizedTypeName = typeName.trim(); + if (normalizedTypeName.isEmpty) return null; + + final scopeResult = library.firstFragment.scope.lookup(normalizedTypeName); + final scopeType = _resolveTypeFromElement(scopeResult.getter); + if (scopeType != null) { + return scopeType; + } + + // Try import namespaces directly as a fallback for simple imported names. + for (final import in library.firstFragment.libraryImports) { + final importedElement = import.namespace.get2(normalizedTypeName); + final importedType = _resolveTypeFromElement(importedElement); + if (importedType != null) { + return importedType; + } + } + + // Last-resort local lookup. + for (final enumElement in library.enums) { + if (enumElement.name3 == normalizedTypeName) { + return enumElement.thisType; + } + } + for (final classElement in library.classes) { + if (classElement.name3 == normalizedTypeName) { + return classElement.thisType; + } + } + + return null; + } + + DartType? _resolveTypeFromElement(Element2? element) { + if (element is EnumElement2) { + return element.thisType; + } + + if (element is ClassElement2) { + return element.thisType; + } + + if (element is TypeAliasElement2) { + final aliasedType = element.aliasedType; + if (aliasedType is InterfaceType) { + return aliasedType; + } + } + + return null; + } + _ListElementRef _resolveListElementRef(Expression firstArg) { if (firstArg is MethodInvocation) { final baseInvocation = _findBaseAckInvocation(firstArg); @@ -1112,6 +1468,10 @@ class SchemaAstAnalyzer { return 'List<$nestedType>'; } + if (methodName == 'enumValues') { + return _extractEnumTypeNameFromInvocation(ref.ackBase!) ?? 'dynamic'; + } + // Map primitive schema types return _mapSchemaMethodToType(methodName); } @@ -1200,32 +1560,7 @@ class SchemaAstAnalyzer { customTypeName: customTypeName, ); - // Strategy 1: Try to extract from type arguments: Ack.enumValues([...]) - final typeArgs = invocation.typeArguments?.arguments; - String? enumTypeName; - - if (typeArgs != null && typeArgs.isNotEmpty) { - // Get the first type argument (the enum type) - final typeArg = typeArgs.first; - enumTypeName = typeArg.toString(); - } else { - // Strategy 2: Try to infer from argument list: Ack.enumValues(UserRole.values) - final args = invocation.argumentList.arguments; - if (args.isNotEmpty) { - final firstArg = args.first; - - // Check if it's EnumType.values (PrefixedIdentifier) - if (firstArg is PrefixedIdentifier) { - final prefix = firstArg.prefix.name; - final identifier = firstArg.identifier.name; - - if (identifier == 'values') { - // UserRole.values → use 'UserRole' - enumTypeName = prefix; - } - } - } - } + final enumTypeName = _extractEnumTypeNameFromInvocation(invocation); // If we couldn't extract the enum type, throw an error if (enumTypeName == null) { @@ -1262,24 +1597,15 @@ class SchemaAstAnalyzer { /// Used for generating string representations of types in list element contexts. /// For nested lists, this function is called recursively via [_parseListSchema]. String _mapSchemaMethodToType(String methodName) { - switch (methodName) { - case 'string': - return 'String'; - case 'integer': - return 'int'; - case 'double': - return 'double'; - case 'boolean': - return 'bool'; - case 'object': - return _kMapType; - case 'list': - // Note: Nested lists are handled by _parseListSchema recursively - // This case exists for consistency but should not be reached in normal flow - return 'List'; - default: - return 'dynamic'; - } + return switch (methodName) { + 'string' || 'enumString' || 'literal' => 'String', + 'integer' => 'int', + 'double' => 'double', + 'boolean' => 'bool', + 'object' => _kMapType, + 'list' => 'List', + _ => 'dynamic', + }; } /// Validates that a field name is a valid Dart identifier diff --git a/packages/ack_generator/lib/src/builders/type_builder.dart b/packages/ack_generator/lib/src/builders/type_builder.dart index fc670e87..c20390ee 100644 --- a/packages/ack_generator/lib/src/builders/type_builder.dart +++ b/packages/ack_generator/lib/src/builders/type_builder.dart @@ -727,7 +727,8 @@ ${cases.join(',\n')}, // Enums if (field.isEnum) { - return field.type.getDisplayString(withNullability: false); + return field.displayTypeOverride ?? + field.type.getDisplayString(withNullability: false); } // Lists @@ -803,6 +804,10 @@ ${cases.join(',\n')}, return kMapType; } + if (field.collectionElementDisplayTypeOverride != null) { + return field.collectionElementDisplayTypeOverride!; + } + if (field.type is! ParameterizedType) return 'dynamic'; final paramType = field.type as ParameterizedType; @@ -963,11 +968,7 @@ ${cases.join(',\n')}, return Parameter( (p) => p ..name = field.name - ..type = _referenceFromDartType( - field.type, - forceNullable: true, - stripNullability: true, - ) + ..type = _buildCopyWithParameterType(field) ..named = true, ); }).toList(); @@ -1001,6 +1002,28 @@ ${assignments.join(',\n')}, ); } + Reference _buildCopyWithParameterType(FieldInfo field) { + if (field.isEnum && field.displayTypeOverride != null) { + return _typeReference(field.displayTypeOverride!, isNullable: true); + } + + if ((field.isList || field.isSet) && + field.collectionElementDisplayTypeOverride != null) { + final collectionType = field.isSet ? 'Set' : 'List'; + return _typeReference( + collectionType, + types: [_typeReference(field.collectionElementDisplayTypeOverride!)], + isNullable: true, + ); + } + + return _referenceFromDartType( + field.type, + forceNullable: true, + stripNullability: true, + ); + } + /// Builds the `args` getter that returns additional properties /// /// Returns a Map containing only properties that are not explicitly diff --git a/packages/ack_generator/lib/src/models/field_info.dart b/packages/ack_generator/lib/src/models/field_info.dart index eece63f2..1b38687f 100644 --- a/packages/ack_generator/lib/src/models/field_info.dart +++ b/packages/ack_generator/lib/src/models/field_info.dart @@ -27,6 +27,13 @@ class FieldInfo { /// properly typed getters like `AddressType get address`. final String? nestedSchemaRef; + /// Optional display type override used when source qualification matters + /// (e.g., `alias.UserRole` from a prefixed import). + final String? displayTypeOverride; + + /// Optional collection element display type override for list/set fields. + final String? collectionElementDisplayTypeOverride; + const FieldInfo({ required this.name, required this.jsonKey, @@ -37,6 +44,8 @@ class FieldInfo { this.description, this.listElementSchemaRef, this.nestedSchemaRef, + this.displayTypeOverride, + this.collectionElementDisplayTypeOverride, }); /// Whether this field references another schema model diff --git a/packages/ack_generator/test/bugs/schema_variable_bugs_test.dart b/packages/ack_generator/test/bugs/schema_variable_bugs_test.dart index b46e6f2a..64956101 100644 --- a/packages/ack_generator/test/bugs/schema_variable_bugs_test.dart +++ b/packages/ack_generator/test/bugs/schema_variable_bugs_test.dart @@ -802,4 +802,377 @@ final keywordSchema = Ack.object({ } }); }); + + group('Enum, literal, and enumValues as fields inside Ack.object()', () { + test('handles Ack.enumString() as field in Ack.object()', () async { + final assets = { + ...allAssets, + 'test_pkg|lib/schema.dart': ''' +import 'package:ack/ack.dart'; +import 'package:ack_annotations/ack_annotations.dart'; + +@AckType() +final reviewSchema = Ack.object({ + 'file': Ack.string(), + 'severity': Ack.enumString(['error', 'warning', 'info']), + 'message': Ack.string(), +}); +''', + }; + + await resolveSources(assets, (resolver) async { + final library = await resolver.libraryFor( + AssetId('test_pkg', 'lib/schema.dart'), + ); + final schemaVar = library.topLevelVariables + .whereType() + .firstWhere((e) => e.name3 == 'reviewSchema'); + + final analyzer = SchemaAstAnalyzer(); + final modelInfo = analyzer.analyzeSchemaVariable(schemaVar); + + expect(modelInfo, isNotNull); + expect(modelInfo!.fields.length, 3); + + final severityField = modelInfo.fields.firstWhere( + (f) => f.name == 'severity', + ); + + expect(severityField.type.isDartCoreString, isTrue); + }); + }); + + test('handles Ack.enumString() with optional/nullable modifiers', () async { + final assets = { + ...allAssets, + 'test_pkg|lib/schema.dart': ''' +import 'package:ack/ack.dart'; +import 'package:ack_annotations/ack_annotations.dart'; + +@AckType() +final formSchema = Ack.object({ + 'priority': Ack.enumString(['low', 'medium', 'high']).optional().nullable(), +}); +''', + }; + + await resolveSources(assets, (resolver) async { + final library = await resolver.libraryFor( + AssetId('test_pkg', 'lib/schema.dart'), + ); + final schemaVar = library.topLevelVariables + .whereType() + .firstWhere((e) => e.name3 == 'formSchema'); + + final analyzer = SchemaAstAnalyzer(); + final modelInfo = analyzer.analyzeSchemaVariable(schemaVar); + + expect(modelInfo, isNotNull); + + final priorityField = modelInfo!.fields.firstWhere( + (f) => f.name == 'priority', + ); + + expect(priorityField.type.isDartCoreString, isTrue); + expect(priorityField.isRequired, isFalse); + expect(priorityField.isNullable, isTrue); + }); + }); + + test('handles Ack.literal() as field in Ack.object()', () async { + final assets = { + ...allAssets, + 'test_pkg|lib/schema.dart': ''' +import 'package:ack/ack.dart'; +import 'package:ack_annotations/ack_annotations.dart'; + +@AckType() +final eventSchema = Ack.object({ + 'type': Ack.literal('click'), + 'target': Ack.string(), +}); +''', + }; + + await resolveSources(assets, (resolver) async { + final library = await resolver.libraryFor( + AssetId('test_pkg', 'lib/schema.dart'), + ); + final schemaVar = library.topLevelVariables + .whereType() + .firstWhere((e) => e.name3 == 'eventSchema'); + + final analyzer = SchemaAstAnalyzer(); + final modelInfo = analyzer.analyzeSchemaVariable(schemaVar); + + expect(modelInfo, isNotNull); + + final typeField = modelInfo!.fields.firstWhere( + (f) => f.name == 'type', + ); + + expect(typeField.type.isDartCoreString, isTrue); + }); + }); + + test('handles Ack.enumValues() as field in Ack.object()', () async { + final assets = { + ...allAssets, + 'test_pkg|lib/schema.dart': ''' +import 'package:ack/ack.dart'; +import 'package:ack_annotations/ack_annotations.dart'; + +enum UserRole { admin, editor, viewer } + +@AckType() +final userSchema = Ack.object({ + 'name': Ack.string(), + 'role': Ack.enumValues(UserRole.values), +}); +''', + }; + + await resolveSources(assets, (resolver) async { + final library = await resolver.libraryFor( + AssetId('test_pkg', 'lib/schema.dart'), + ); + final schemaVar = library.topLevelVariables + .whereType() + .firstWhere((e) => e.name3 == 'userSchema'); + + final analyzer = SchemaAstAnalyzer(); + final modelInfo = analyzer.analyzeSchemaVariable(schemaVar); + + expect(modelInfo, isNotNull); + expect(modelInfo!.fields.length, 2); + + final roleField = modelInfo.fields.firstWhere( + (f) => f.name == 'role', + ); + + expect(roleField.type.element3, isNotNull); + expect( + roleField.type.getDisplayString(withNullability: false), + equals('UserRole'), + ); + }); + }); + + test( + 'handles Ack.enumValues() with optional modifier in Ack.object()', + () async { + final assets = { + ...allAssets, + 'test_pkg|lib/schema.dart': ''' +import 'package:ack/ack.dart'; +import 'package:ack_annotations/ack_annotations.dart'; + +enum Priority { low, medium, high, critical } + +@AckType() +final taskSchema = Ack.object({ + 'title': Ack.string(), + 'priority': Ack.enumValues(Priority.values).optional(), +}); +''', + }; + + await resolveSources(assets, (resolver) async { + final library = await resolver.libraryFor( + AssetId('test_pkg', 'lib/schema.dart'), + ); + final schemaVar = library.topLevelVariables + .whereType() + .firstWhere((e) => e.name3 == 'taskSchema'); + + final analyzer = SchemaAstAnalyzer(); + final modelInfo = analyzer.analyzeSchemaVariable(schemaVar); + + expect(modelInfo, isNotNull); + + final priorityField = modelInfo!.fields.firstWhere( + (f) => f.name == 'priority', + ); + + expect(priorityField.isRequired, isFalse); + expect( + priorityField.type.getDisplayString(withNullability: false), + equals('Priority'), + ); + }); + }, + ); + + test('handles Ack.list(Ack.enumString()) in Ack.object()', () async { + final assets = { + ...allAssets, + 'test_pkg|lib/schema.dart': ''' +import 'package:ack/ack.dart'; +import 'package:ack_annotations/ack_annotations.dart'; + +@AckType() +final configSchema = Ack.object({ + 'tags': Ack.list(Ack.enumString(['a', 'b', 'c'])), +}); +''', + }; + + await resolveSources(assets, (resolver) async { + final library = await resolver.libraryFor( + AssetId('test_pkg', 'lib/schema.dart'), + ); + final schemaVar = library.topLevelVariables + .whereType() + .firstWhere((e) => e.name3 == 'configSchema'); + + final analyzer = SchemaAstAnalyzer(); + final modelInfo = analyzer.analyzeSchemaVariable(schemaVar); + + expect(modelInfo, isNotNull); + + final tagsField = modelInfo!.fields.firstWhere( + (f) => f.name == 'tags', + ); + + expect(tagsField.type.isDartCoreList, isTrue); + + final listType = tagsField.type as InterfaceType; + final elementType = listType.typeArguments.first; + expect(elementType.isDartCoreString, isTrue); + }); + }); + + test('handles Ack.list(Ack.enumValues()) in Ack.object()', () async { + final assets = { + ...allAssets, + 'test_pkg|lib/schema.dart': ''' +import 'package:ack/ack.dart'; +import 'package:ack_annotations/ack_annotations.dart'; + +enum UserRole { admin, editor, viewer } + +@AckType() +final teamSchema = Ack.object({ + 'name': Ack.string(), + 'roles': Ack.list(Ack.enumValues(UserRole.values)), +}); +''', + }; + + await resolveSources(assets, (resolver) async { + final library = await resolver.libraryFor( + AssetId('test_pkg', 'lib/schema.dart'), + ); + final schemaVar = library.topLevelVariables + .whereType() + .firstWhere((e) => e.name3 == 'teamSchema'); + + final analyzer = SchemaAstAnalyzer(); + final modelInfo = analyzer.analyzeSchemaVariable(schemaVar); + + expect(modelInfo, isNotNull); + + final rolesField = modelInfo!.fields.firstWhere( + (f) => f.name == 'roles', + ); + + expect(rolesField.type.isDartCoreList, isTrue); + + final listType = rolesField.type as InterfaceType; + final elementType = listType.typeArguments.first; + expect( + elementType.getDisplayString(withNullability: false), + equals('UserRole'), + ); + }); + }); + + test('handles Ack.list(Ack.literal()) in Ack.object()', () async { + final assets = { + ...allAssets, + 'test_pkg|lib/schema.dart': ''' +import 'package:ack/ack.dart'; +import 'package:ack_annotations/ack_annotations.dart'; + +@AckType() +final actionSchema = Ack.object({ + 'types': Ack.list(Ack.literal('click')), + 'name': Ack.string(), +}); +''', + }; + + await resolveSources(assets, (resolver) async { + final library = await resolver.libraryFor( + AssetId('test_pkg', 'lib/schema.dart'), + ); + final schemaVar = library.topLevelVariables + .whereType() + .firstWhere((e) => e.name3 == 'actionSchema'); + + final analyzer = SchemaAstAnalyzer(); + final modelInfo = analyzer.analyzeSchemaVariable(schemaVar); + + expect(modelInfo, isNotNull); + + final typesField = modelInfo!.fields.firstWhere( + (f) => f.name == 'types', + ); + + expect(typesField.type.isDartCoreList, isTrue); + + final listType = typesField.type as InterfaceType; + final elementType = listType.typeArguments.first; + expect(elementType.isDartCoreString, isTrue); + }); + }); + + test('handles Ack.enumValues().nullable() without optional()', + () async { + final assets = { + ...allAssets, + 'test_pkg|lib/schema.dart': ''' +import 'package:ack/ack.dart'; +import 'package:ack_annotations/ack_annotations.dart'; + +enum UserRole { admin, editor, viewer } + +@AckType() +final profileSchema = Ack.object({ + 'name': Ack.string(), + 'role': Ack.enumValues(UserRole.values).nullable(), +}); +''', + }; + + await resolveSources(assets, (resolver) async { + final library = await resolver.libraryFor( + AssetId('test_pkg', 'lib/schema.dart'), + ); + final schemaVar = library.topLevelVariables + .whereType() + .firstWhere((e) => e.name3 == 'profileSchema'); + + final analyzer = SchemaAstAnalyzer(); + final modelInfo = analyzer.analyzeSchemaVariable(schemaVar); + + expect(modelInfo, isNotNull); + + final roleField = modelInfo!.fields.firstWhere( + (f) => f.name == 'role', + ); + + expect( + roleField.type.getDisplayString(withNullability: false), + equals('UserRole'), + ); + expect(roleField.isNullable, isTrue); + expect( + roleField.isRequired, + isTrue, + reason: 'nullable() alone should not make the field optional', + ); + }); + }); + }); } diff --git a/packages/ack_generator/test/integration/ack_type_enum_literal_fields_test.dart b/packages/ack_generator/test/integration/ack_type_enum_literal_fields_test.dart new file mode 100644 index 00000000..0b2b7276 --- /dev/null +++ b/packages/ack_generator/test/integration/ack_type_enum_literal_fields_test.dart @@ -0,0 +1,439 @@ +import 'package:ack_generator/builder.dart'; +import 'package:build/build.dart'; +import 'package:build_test/build_test.dart'; +import 'package:test/test.dart'; + +import '../test_utils/test_assets.dart'; + +void main() { + group('@AckType with enumString, literal, and enumValues fields', () { + test('enumString field generates String getter', () async { + final builder = ackGenerator(BuilderOptions.empty); + + await testBuilder( + builder, + { + ...allAssets, + 'test_pkg|lib/schema.dart': ''' +import 'package:ack/ack.dart'; +import 'package:ack_annotations/ack_annotations.dart'; + +@AckType() +final reviewSchema = Ack.object({ + 'file': Ack.string(), + 'severity': Ack.enumString(['error', 'warning', 'info']), + 'message': Ack.string(), +}); +''', + }, + outputs: { + 'test_pkg|lib/schema.g.dart': decodedMatches( + allOf([ + contains('extension type ReviewType(Map _data)'), + contains('String get file'), + contains("_data['file'] as String"), + contains('String get severity'), + contains("_data['severity'] as String"), + contains('String get message'), + contains("_data['message'] as String"), + ]), + ), + }, + ); + }); + + test('literal field generates String getter', () async { + final builder = ackGenerator(BuilderOptions.empty); + + await testBuilder( + builder, + { + ...allAssets, + 'test_pkg|lib/schema.dart': ''' +import 'package:ack/ack.dart'; +import 'package:ack_annotations/ack_annotations.dart'; + +@AckType() +final eventSchema = Ack.object({ + 'type': Ack.literal('click'), + 'target': Ack.string(), +}); +''', + }, + outputs: { + 'test_pkg|lib/schema.g.dart': decodedMatches( + allOf([ + contains('extension type EventType(Map _data)'), + contains('String get type'), + contains("_data['type'] as String"), + contains('String get target'), + contains("_data['target'] as String"), + ]), + ), + }, + ); + }); + + test('enumValues field generates enum type getter', () async { + final builder = ackGenerator(BuilderOptions.empty); + + await testBuilder( + builder, + { + ...allAssets, + 'test_pkg|lib/schema.dart': ''' +import 'package:ack/ack.dart'; +import 'package:ack_annotations/ack_annotations.dart'; + +enum UserRole { admin, editor, viewer } + +@AckType() +final userSchema = Ack.object({ + 'name': Ack.string(), + 'role': Ack.enumValues(UserRole.values), +}); +''', + }, + outputs: { + 'test_pkg|lib/schema.g.dart': decodedMatches( + allOf([ + contains('extension type UserType(Map _data)'), + contains('String get name'), + contains('UserRole get role'), + contains("_data['role'] as UserRole"), + ]), + ), + }, + ); + }); + + test( + 'enumString with optional().nullable() generates String? getter', + () async { + final builder = ackGenerator(BuilderOptions.empty); + + await testBuilder( + builder, + { + ...allAssets, + 'test_pkg|lib/schema.dart': ''' +import 'package:ack/ack.dart'; +import 'package:ack_annotations/ack_annotations.dart'; + +@AckType() +final formSchema = Ack.object({ + 'title': Ack.string(), + 'priority': Ack.enumString(['low', 'medium', 'high']).optional().nullable(), +}); +''', + }, + outputs: { + 'test_pkg|lib/schema.g.dart': decodedMatches( + allOf([ + contains('extension type FormType(Map _data)'), + contains('String get title'), + contains('String? get priority'), + contains("_data['priority'] as String?"), + ]), + ), + }, + ); + }, + ); + + test('enumValues with optional() generates T? getter', () async { + final builder = ackGenerator(BuilderOptions.empty); + + await testBuilder( + builder, + { + ...allAssets, + 'test_pkg|lib/schema.dart': ''' +import 'package:ack/ack.dart'; +import 'package:ack_annotations/ack_annotations.dart'; + +enum Priority { low, medium, high, critical } + +@AckType() +final taskSchema = Ack.object({ + 'title': Ack.string(), + 'priority': Ack.enumValues(Priority.values).optional(), +}); +''', + }, + outputs: { + 'test_pkg|lib/schema.g.dart': decodedMatches( + allOf([ + contains('extension type TaskType(Map _data)'), + contains('String get title'), + contains('Priority? get priority'), + contains("_data['priority'] as Priority?"), + ]), + ), + }, + ); + }); + + test('Ack.list(Ack.enumString()) generates List getter', () async { + final builder = ackGenerator(BuilderOptions.empty); + + await testBuilder( + builder, + { + ...allAssets, + 'test_pkg|lib/schema.dart': ''' +import 'package:ack/ack.dart'; +import 'package:ack_annotations/ack_annotations.dart'; + +@AckType() +final configSchema = Ack.object({ + 'name': Ack.string(), + 'tags': Ack.list(Ack.enumString(['a', 'b', 'c'])), +}); +''', + }, + outputs: { + 'test_pkg|lib/schema.g.dart': decodedMatches( + allOf([ + contains('extension type ConfigType(Map _data)'), + contains('String get name'), + contains('List get tags'), + contains("(_data['tags'] as List).cast()"), + ]), + ), + }, + ); + }); + + test('Ack.list(Ack.enumValues()) generates List getter', () async { + final builder = ackGenerator(BuilderOptions.empty); + + await testBuilder( + builder, + { + ...allAssets, + 'test_pkg|lib/schema.dart': ''' +import 'package:ack/ack.dart'; +import 'package:ack_annotations/ack_annotations.dart'; + +enum UserRole { admin, editor, viewer } + +@AckType() +final teamSchema = Ack.object({ + 'name': Ack.string(), + 'roles': Ack.list(Ack.enumValues(UserRole.values)), +}); +''', + }, + outputs: { + 'test_pkg|lib/schema.g.dart': decodedMatches( + allOf([ + contains('extension type TeamType(Map _data)'), + contains('String get name'), + contains('List get roles'), + contains("(_data['roles'] as List).cast()"), + ]), + ), + }, + ); + }); + + test( + 'enumValues with imported prefixed enum keeps prefixed field type', + () async { + final builder = ackGenerator(BuilderOptions.empty); + + await testBuilder( + builder, + { + ...allAssets, + 'test_pkg|lib/enums.dart': ''' +enum UserRole { admin, editor, viewer } +''', + 'test_pkg|lib/schema.dart': ''' +import 'package:ack/ack.dart'; +import 'package:ack_annotations/ack_annotations.dart'; +import 'enums.dart' as models; + +@AckType() +final userSchema = Ack.object({ + 'role': Ack.enumValues(models.UserRole.values), +}); +''', + }, + outputs: { + 'test_pkg|lib/schema.g.dart': decodedMatches( + allOf([ + contains('extension type UserType(Map _data)'), + contains('models.UserRole get role'), + contains("_data['role'] as models.UserRole"), + contains('models.UserRole? role'), + ]), + ), + }, + ); + }, + ); + + test( + 'top-level list enumValues preserves imported prefixed enum type', + () async { + final builder = ackGenerator(BuilderOptions.empty); + + await testBuilder( + builder, + { + ...allAssets, + 'test_pkg|lib/enums.dart': ''' +enum UserRole { admin, editor, viewer } +''', + 'test_pkg|lib/schema.dart': ''' +import 'package:ack/ack.dart'; +import 'package:ack_annotations/ack_annotations.dart'; +import 'enums.dart' as models; + +@AckType() +final roleListSchema = Ack.list(Ack.enumValues(models.UserRole.values)); +''', + }, + outputs: { + 'test_pkg|lib/schema.g.dart': decodedMatches( + allOf([ + contains( + 'extension type RoleListType(List _value)', + ), + contains('implements List'), + contains('RoleListType(validated as List)'), + ]), + ), + }, + ); + }, + ); + + test( + 'list enumValues with imported prefixed enum keeps prefixed copyWith type', + () async { + final builder = ackGenerator(BuilderOptions.empty); + + await testBuilder( + builder, + { + ...allAssets, + 'test_pkg|lib/enums.dart': ''' +enum UserRole { admin, editor, viewer } +''', + 'test_pkg|lib/schema.dart': ''' +import 'package:ack/ack.dart'; +import 'package:ack_annotations/ack_annotations.dart'; +import 'enums.dart' as models; + +@AckType() +final teamSchema = Ack.object({ + 'roles': Ack.list(Ack.enumValues(models.UserRole.values)), +}); +''', + }, + outputs: { + 'test_pkg|lib/schema.g.dart': decodedMatches( + allOf([ + contains('List get roles'), + contains('List? roles'), + ]), + ), + }, + ); + }, + ); + + test( + 'mixed schema with literal, enumString, enumValues, and string fields', + () async { + final builder = ackGenerator(BuilderOptions.empty); + + await testBuilder( + builder, + { + ...allAssets, + 'test_pkg|lib/schema.dart': ''' +import 'package:ack/ack.dart'; +import 'package:ack_annotations/ack_annotations.dart'; + +enum Color { red, green, blue } + +@AckType() +final widgetSchema = Ack.object({ + 'type': Ack.literal('button'), + 'label': Ack.string(), + 'color': Ack.enumValues(Color.values), + 'style': Ack.enumString(['solid', 'outline', 'ghost']), +}); +''', + }, + outputs: { + 'test_pkg|lib/schema.g.dart': decodedMatches( + allOf([ + contains( + 'extension type WidgetType(Map _data)', + ), + contains('String get type'), + contains("_data['type'] as String"), + contains('String get label'), + contains("_data['label'] as String"), + contains('Color get color'), + contains("_data['color'] as Color"), + contains('String get style'), + contains("_data['style'] as String"), + ]), + ), + }, + ); + }, + ); + + test( + 'enumValues infers enum type from variable and property inputs', + () async { + final builder = ackGenerator(BuilderOptions.empty); + + await testBuilder( + builder, + { + ...allAssets, + 'test_pkg|lib/schema.dart': ''' +import 'package:ack/ack.dart'; +import 'package:ack_annotations/ack_annotations.dart'; + +enum UserRole { admin, editor, viewer } + +final roleValues = UserRole.values; + +class RoleHolder { + const RoleHolder(this.values); + final List values; +} + +const holder = RoleHolder(UserRole.values); + +@AckType() +final userSchema = Ack.object({ + 'roleFromVar': Ack.enumValues(roleValues), + 'roleFromProp': Ack.enumValues(holder.values), +}); +''', + }, + outputs: { + 'test_pkg|lib/schema.g.dart': decodedMatches( + allOf([ + contains('UserRole get roleFromVar'), + contains("_data['roleFromVar'] as UserRole"), + contains('UserRole get roleFromProp'), + contains("_data['roleFromProp'] as UserRole"), + ]), + ), + }, + ); + }, + ); + }); +} diff --git a/packages/ack_generator/test/integration/ack_type_to_json_test.dart b/packages/ack_generator/test/integration/ack_type_to_json_test.dart new file mode 100644 index 00000000..02b92367 --- /dev/null +++ b/packages/ack_generator/test/integration/ack_type_to_json_test.dart @@ -0,0 +1,48 @@ +import 'package:ack_generator/builder.dart'; +import 'package:build/build.dart'; +import 'package:build_test/build_test.dart'; +import 'package:test/test.dart'; + +import '../test_utils/test_assets.dart'; + +void main() { + group('@AckType toJson generation', () { + test('emits toJson for object, primitive, and collection types', () async { + final builder = ackGenerator(BuilderOptions.empty); + + await testBuilder( + builder, + { + ...allAssets, + 'test_pkg|lib/schema.dart': ''' +import 'package:ack/ack.dart'; +import 'package:ack_annotations/ack_annotations.dart'; + +@AckType() +final userSchema = Ack.object({ + 'name': Ack.string(), +}); + +@AckType() +final passwordSchema = Ack.string(); + +@AckType() +final tagsSchema = Ack.list(Ack.string()); +''', + }, + outputs: { + 'test_pkg|lib/schema.g.dart': decodedMatches( + allOf([ + contains('extension type UserType(Map _data)'), + contains('Map toJson() => _data;'), + contains('extension type PasswordType(String _value)'), + contains('String toJson() => _value;'), + contains('extension type TagsType(List _value)'), + contains('List toJson() => _value;'), + ]), + ), + }, + ); + }); + }); +} diff --git a/packages/ack_generator/test/src/test_utilities.dart b/packages/ack_generator/test/src/test_utilities.dart index b47aa6ff..cac1d0a8 100644 --- a/packages/ack_generator/test/src/test_utilities.dart +++ b/packages/ack_generator/test/src/test_utilities.dart @@ -50,6 +50,12 @@ class MockFieldInfo implements FieldInfo { @override final String? nestedSchemaRef; + @override + final String? displayTypeOverride; + + @override + final String? collectionElementDisplayTypeOverride; + final String typeName; final String? listItemTypeName; final String? mapKeyTypeName; @@ -74,6 +80,8 @@ class MockFieldInfo implements FieldInfo { this.mapValueTypeName, this.listElementSchemaRef, this.nestedSchemaRef, + this.displayTypeOverride, + this.collectionElementDisplayTypeOverride, }) : jsonKey = name; @override diff --git a/packages/ack_generator/test/test_utils/test_assets.dart b/packages/ack_generator/test/test_utils/test_assets.dart index de0c71d2..5b09d66f 100644 --- a/packages/ack_generator/test/test_utils/test_assets.dart +++ b/packages/ack_generator/test/test_utils/test_assets.dart @@ -134,6 +134,10 @@ class Ack { discriminatorKey: discriminatorKey, schemas: schemas, ); + + static StringSchema literal(String value) => const StringSchema(); + static StringSchema enumString(List values) => const StringSchema(); + static EnumSchema enumValues(List values) => EnumSchema(); } abstract class AckSchema { @@ -259,11 +263,20 @@ class ObjectSchema extends AckSchema> { extension ObjectSchemaExtensions on ObjectSchema { ObjectSchema passthrough() => copyWith(additionalProperties: true); } +class EnumSchema extends AckSchema { + EnumSchema(); + EnumSchema nullable() => this; + EnumSchema optional() => this; + EnumSchema describe(String description) => this; + + @override + Map toJsonSchema() => {'type': 'string', 'enum': []}; +} class DiscriminatedSchema extends AckSchema> { final String discriminatorKey; final Map schemas; const DiscriminatedSchema({required this.discriminatorKey, required this.schemas}); - + @override Map toJsonSchema() => { 'oneOf': schemas.values.map((schema) => schema.toJsonSchema()).toList(), diff --git a/pubspec.yaml b/pubspec.yaml index cf40b17c..46d39726 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -56,6 +56,7 @@ melos: - ack_firebase_ai - ack_json_schema_builder - ack_example + - ack_annotations fix: run: melos exec -- "dart fix --apply"