From 06b8bff59bc8c7aaa3de8af57e8fb6e38fff2bd1 Mon Sep 17 00:00:00 2001 From: parables <38154990+Parables@users.noreply.github.com> Date: Tue, 8 Nov 2022 04:35:33 +0000 Subject: [PATCH 1/3] wip: fix dart keywords issue --- .../src/freezed-declaration-blocks/parameter-block.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/parameter-block.ts b/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/parameter-block.ts index 258f616b2..2c0a79a2a 100644 --- a/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/parameter-block.ts +++ b/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/parameter-block.ts @@ -4,6 +4,8 @@ import { camelCase } from 'change-case-all'; import { ApplyDecoratorOn, FlutterFreezedPluginConfig } from '../config.js'; import { getCustomDecorators, transformCustomDecorators, FieldType, FreezedConfigValue, NodeType } from '../utils.js'; +// TODO: Fix Dart keywords issue + /** * maps GraphQL scalar types to Dart's scalar types */ From c87b6c7bb6947c7fb4f20fb06fc3a3bf5b6669c7 Mon Sep 17 00:00:00 2001 From: parables <38154990+Parables@users.noreply.github.com> Date: Fri, 11 Nov 2022 04:14:30 +0000 Subject: [PATCH 2/3] feat: :sparkles: fixed broken plugin... major refactoring The previous version used classes to store instances of the Freezed Blocks and mutated its properties with setters which made it hard to test the plugin and also made it hard to identify state changes caused by side-effects. This version throws all the setters and use static methods and functions that only do one thing DefaultFreezedConfig and DefaultFlutterFreezedPluginConfig classes have been removed, all setters on the blocks have been removed --- .../plugins/dart/flutter-freezed/package.json | 4 +- .../dart/flutter-freezed/src/config.ts | 197 ++++- .../dart/flutter-freezed/src/cspell.json | 9 + .../freezed-declaration-blocks/class-block.ts | 311 +------- .../factory-block.ts | 199 +---- .../src/freezed-declaration-blocks/index.ts | 6 +- .../parameter-block.ts | 223 +----- .../plugins/dart/flutter-freezed/src/index.ts | 31 +- .../flutter-freezed/src/schema-visitor.ts | 20 +- .../plugins/dart/flutter-freezed/src/utils.ts | 724 ++++++++++++++---- .../dart/flutter-freezed/tests/config.spec.ts | 489 ++---------- .../dart/flutter-freezed/tests/config.ts | 20 +- .../dart/flutter-freezed/tests/plugin.spec.ts | 284 ------- .../dart/flutter-freezed/tests/plugin.ts | 211 ----- .../dart/flutter-freezed/tests/schema.ts | 207 ++--- .../dart/flutter-freezed/tests/utils.spec.ts | 605 +++++++++++++-- 16 files changed, 1580 insertions(+), 1960 deletions(-) create mode 100644 packages/plugins/dart/flutter-freezed/src/cspell.json delete mode 100644 packages/plugins/dart/flutter-freezed/tests/plugin.spec.ts delete mode 100644 packages/plugins/dart/flutter-freezed/tests/plugin.ts diff --git a/packages/plugins/dart/flutter-freezed/package.json b/packages/plugins/dart/flutter-freezed/package.json index f03d5625c..d8e136794 100644 --- a/packages/plugins/dart/flutter-freezed/package.json +++ b/packages/plugins/dart/flutter-freezed/package.json @@ -17,8 +17,8 @@ "@graphql-codegen/schema-ast": "^2.5.0", "@graphql-codegen/visitor-plugin-common": "2.13.1", "auto-bind": "~4.0.0", - "tslib": "~2.4.0", - "change-case-all": "1.0.14" + "change-case-all": "1.0.14", + "tslib": "~2.4.0" }, "peerDependencies": { "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" diff --git a/packages/plugins/dart/flutter-freezed/src/config.ts b/packages/plugins/dart/flutter-freezed/src/config.ts index 59908a1f9..ca3107a04 100644 --- a/packages/plugins/dart/flutter-freezed/src/config.ts +++ b/packages/plugins/dart/flutter-freezed/src/config.ts @@ -13,6 +13,177 @@ export type ApplyDecoratorOn = | 'union_factory_parameter' | 'merged_input_parameter'; +/** + * maps GraphQL scalar types to Dart's scalar types + */ +export const DART_SCALARS: Record = { + ID: 'String', + String: 'String', + Boolean: 'bool', + Int: 'int', + Float: 'double', + DateTime: 'DateTime', +}; + +// TODO: get this from config or use default or build withPrefix or withSuffix +export const DART_KEYWORDS = { + abstract: 'built-in', + else: 'reserved', + import: 'built-in', + show: 'context', + as: 'built-in', + enum: 'reserved', + in: 'reserved', + static: 'built-in', + assert: 'reserved', + export: 'built-in', + interface: 'built-in', + super: 'reserved', + async: 'context', + extends: 'reserved', + is: 'reserved', + switch: 'reserved', + await: 'async-reserved', + extension: 'built-in', + late: 'built-in', + sync: 'context', + break: 'reserved', + external: 'built-in', + library: 'built-in', + this: 'reserved', + case: 'reserved', + factory: 'built-in', + mixin: 'built-in', + throw: 'reserved', + catch: 'reserved', + false: 'reserved', + new: 'reserved', + true: 'reserved', + class: 'reserved', + final: 'reserved', + null: 'reserved', + try: 'reserved', + const: 'reserved', + finally: 'reserved', + on: 'context', + typedef: 'built-in', + continue: 'reserved', + for: 'reserved', + operator: 'built-in', + var: 'reserved', + covariant: 'built-in', + Function: 'built-in', + part: 'built-in', + void: 'reserved', + default: 'reserved', + get: 'built-in', + required: 'built-in', + while: 'reserved', + deferred: 'built-in', + hide: 'context', + rethrow: 'reserved', + with: 'reserved', + do: 'reserved', + if: 'reserved', + return: 'reserved', + yield: 'async-reserved', + dynamic: 'built-in', + implements: 'built-in', + set: 'built-in', + // built-in types + int: 'reserved', + double: 'reserved', + String: 'reserved', + bool: 'reserved', + List: 'reserved', + Set: 'reserved', + Map: 'reserved', + Runes: 'reserved', + Symbol: 'reserved', + Object: 'reserved', + Null: 'reserved', + Never: 'reserved', + Enum: 'reserved', + Future: 'reserved', + Iterable: 'reserved', +}; + +export type DartKeyword = keyof typeof DART_KEYWORDS; + +export type DartKeywordType = 'built-in' | 'context' | 'reserved' | 'async-reserved'; + +export type DartIdentifierCasing = 'snake_case' | 'camelCase' | 'PascalCase'; + +export type DartKeywordConfig = { + /** + * @name dartKeywordEscapeCasing + * @description after escaping a valid dart keyword, this option transforms the casing to `snake_cased`, `camelCase` or `PascalCase`. Defaults to `undefined` to leave the casing as it is. + * @default undefined + * @see_also [escapeDartKeywords, dartKeywordEscapePrefix] + * + * ```yaml + * generates: + * flutter_app/lib/data/models/app_models.dart + * plugins: + * - flutter-freezed + * config: + * dartKeywordEscapeCasing: camelCase + * + * ``` + */ + + dartKeywordEscapeCasing?: DartIdentifierCasing; + + /** + * @name dartKeywordEscapePrefix + * @description prefix GraphQL type and field names that are valid dart keywords. Don't use only a underscore(`_`) as the `dartKeywordEscapePrefix` since it will make that identifier hidden or produce unexpected results. However, if you would want to change the case after escaping the keyword with `dartKeywordEscapeCasing`, you may use either an `_`, `-` or an empty space ` `. + * @default undefined + * @see_also [escapeDartKeywords, dartKeywordEscapeSuffix] + * + * @exampleMarkdown + * ```yaml + * generates: + * flutter_app/lib/data/models/app_models.dart + * plugins: + * - flutter-freezed + * config: + * dartKeywordEscapePrefix: "k_" + * # Example: let keyword = 'in' + * # dartKeywordEscapeCasing === 'snake_case' => 'k_in' + * # dartKeywordEscapeCasing === 'camelCase' => 'kIn' + * # dartKeywordEscapeCasing === 'PascalCase' => 'KIn' + * # dartKeywordEscapeCasing === undefined => 'k_in' + * + * ``` + */ + + dartKeywordEscapePrefix?: string; + + /** + * @name dartKeywordEscapeSuffix + * @description suffix GraphQL type and field names that are valid dart keywords. If the value of `dartKeywordEscapeSuffix` is an `_` and if `dartKeywordEscapeCasing` is `snake_case` or `camelCase`, then the casing will be ignored because it will remove the trailing `_` making the escapedKeyword invalid again + * @default "_" + * @see_also [escapeDartKeywords, dartKeywordEscapePrefix] + * + * ```yaml + * generates: + * flutter_app/lib/data/models/app_models.dart + * plugins: + * - flutter-freezed + * config: + * dartKeywordEscapeSuffix: "_k" or using the default '_' + * # Example: let keyword = 'in' + * # dartKeywordEscapeCasing === 'snake_case'=> 'in_k' or 'in_' // ignored casing + * # dartKeywordEscapeCasing === 'camelCase' =>'inK' or in_ // ignored casing + * # dartKeywordEscapeCasing === 'PascalCase' => 'InK' or 'In' + * # dartKeywordEscapeCasing === undefined => 'in_k' or 'in_' + * + * ``` + */ + + dartKeywordEscapeSuffix?: string; +}; + /** * @name DecoratorToFreezed * @description the value of a `CustomDecorator`. This value specifies how the the decorator should be handled by Freezed @@ -57,7 +228,7 @@ export type CustomDecorator = Record; * @description configure what Freeze should generate * @default DefaultFreezedConfig */ -export interface FreezedConfig { +export type FreezedConfig = DartKeywordConfig & { /** * @name alwaysUseJsonKeyName * @description Use @JsonKey(name: 'name') even if the name is already camelCased @@ -134,9 +305,10 @@ export interface FreezedConfig { customDecorators?: CustomDecorator; /** - * @name defaultUnionConstructor - * @description generate empty constructors for Union Types and mergedInputs + * @name escapeDartKeywords + * @description wraps dart-language reserved keywords such as `void`, `in` etc with a prefix and/or suffix which can be set by changing `dartKeywordEscapePrefix` and `dartKeywordEscapeSuffix` config values * @default true + * @see_also [dartKeywordEscapePrefix,dartKeywordEscapeSuffix] * * @exampleMarkdown * ```yaml @@ -145,11 +317,18 @@ export interface FreezedConfig { * plugins: * - flutter-freezed * config: - * defaultUnionConstructor: true + * escapeDartKeywords: { + * in: true # becomes `in_`, + * required: { #becomes `argRequired` + * dartKeywordEscapePrefix: "arg_", + * dartKeywordEscapeCasing: camelCase + * } + * } + * * ``` */ - defaultUnionConstructor?: boolean; + escapeDartKeywords?: boolean | Record; /** * @name equal @@ -319,13 +498,13 @@ export interface FreezedConfig { */ unionValueCase?: 'FreezedUnionCase.camel' | 'FreezedUnionCase.pascal'; -} +}; /** * @name FieldConfig * @description configuration for the field */ -export interface FieldConfig { +export type FieldConfig = { /** * @name final * @description marks a field as final @@ -380,7 +559,7 @@ export interface FieldConfig { */ customDecorators?: CustomDecorator; -} +}; /** * @name TypeSpecificFreezedConfig @@ -463,7 +642,7 @@ export interface FlutterFreezedPluginConfig /* extends TypeScriptPluginConfig */ * ``` */ - fileName?: string; + fileName: string; /** * @name globalFreezedConfig diff --git a/packages/plugins/dart/flutter-freezed/src/cspell.json b/packages/plugins/dart/flutter-freezed/src/cspell.json new file mode 100644 index 000000000..b28ed8bb7 --- /dev/null +++ b/packages/plugins/dart/flutter-freezed/src/cspell.json @@ -0,0 +1,9 @@ +{ + "version": "0.2", + "ignorePaths": [], + "dictionaryDefinitions": [], + "dictionaries": [], + "words": ["endregion", "Unfreezed"], + "ignoreWords": [], + "import": [] +} diff --git a/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/class-block.ts b/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/class-block.ts index cd8cff32f..61ab7a790 100644 --- a/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/class-block.ts +++ b/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/class-block.ts @@ -1,303 +1,26 @@ -import { indent } from '@graphql-codegen/visitor-plugin-common'; -import { EnumValueDefinitionNode, FieldDefinitionNode, InputValueDefinitionNode, Kind, NamedTypeNode } from 'graphql'; -import { camelCase, pascalCase } from 'change-case-all'; -import { FlutterFreezedPluginConfig } from '../config.js'; +import { Kind } from 'graphql'; +import { FlutterFreezedPluginConfig } from '../config'; import { - getCustomDecorators, - transformCustomDecorators, - FreezedConfigValue, - FreezedFactoryBlockRepository, + buildBlockComment, + buildBlockDecorators, + buildBlockHeader, + buildBlockBody, + buildBlockFooter, NodeType, -} from '../utils.js'; -import { FreezedFactoryBlock } from './factory-block.js'; +} from '../utils'; export class FreezedDeclarationBlock { - /** document the class */ - _comment = ''; + public static build(config: FlutterFreezedPluginConfig, node: NodeType): string { + const blockType = node.kind === Kind.ENUM_TYPE_DEFINITION ? 'enum' : 'class'; + const blockName = node.name.value; - /** a list of decorators to copy paste to the generator */ - _decorators: string[] = []; - - /** the name of the class */ - _name: string | undefined; - - /** a list of default constructor and named Constructors used create a Freezed union/sealed class */ - _factoryBlocks: FreezedFactoryBlock[] = []; - - /** the shape is the content of the block */ - _shape: string | undefined; - - /** the block is the final structure that is generated */ - _block: string | undefined; - - private _freezedConfigValue: FreezedConfigValue; - - constructor( - private _config: FlutterFreezedPluginConfig, - private _freezedFactoryBlockRepository: FreezedFactoryBlockRepository, - private _node: NodeType - ) { - this._config = _config; - this._freezedFactoryBlockRepository = _freezedFactoryBlockRepository; - this._node = _node; - this._freezedConfigValue = new FreezedConfigValue(this._config, this._node.name.value); - } - - public init(): FreezedDeclarationBlock { - if (this._node.kind === Kind.ENUM_TYPE_DEFINITION) { - this.setDecorators().setName().setShape().setBlock(); - return this; - } - this.setComment().setDecorators().setName().setFactoryBlocks().setShape().setBlock(); - return this; - } - - private setComment(): FreezedDeclarationBlock { - const comment = this._node.description?.value; - - if (comment && comment !== null && comment !== '') { - this._comment = `/// ${comment} \n`; - } - - return this; - } - - private getEnumComment(value: EnumValueDefinitionNode): string { - const comment = value.description?.value; - - if (comment && comment !== null && comment !== '') { - return `/// ${comment} \n`; - } - - return ''; - } - - private setDecorators(): FreezedDeclarationBlock { - const name = this._node.name.value; - // determine if should mark as deprecated - const isDeprecated = this._config.typeSpecificFreezedConfig?.[name]?.deprecated; - - this._decorators = - this._node.kind === Kind.ENUM_TYPE_DEFINITION - ? [...transformCustomDecorators(getCustomDecorators(this._config, ['enum'], name), this._node)] - : [ - this.getFreezedDecorator(), - ...transformCustomDecorators(getCustomDecorators(this._config, ['class'], name), this._node), - ]; - - // @deprecated - // if this._decorators doesn't include an @deprecated decorator but the field is marked as @deprecated... - if (!this._decorators.includes('@deprecated') && isDeprecated) { - this._decorators = [...this._decorators, '@deprecated\n']; - } - - return this; - } - - private getFreezedDecorator() { - const use_unfreezed = () => { - if ( - !this._freezedConfigValue.get('immutable') || - (this._node.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION && this._freezedConfigValue.get('mutableInputs')) - ) { - return '@unfreezed\n'; - } - return use_Freezed_or_freezed(); - }; - - const use_Freezed_or_freezed = () => { - if (isCustomizedFreezed()) { - const copyWith = this._freezedConfigValue.get('copyWith'); - const equal = this._freezedConfigValue.get('equal'); - const makeCollectionsUnmodifiable = this._freezedConfigValue.get('makeCollectionsUnmodifiable'); - const unionKey = this._freezedConfigValue.get('unionKey'); - const unionValueCase = this._freezedConfigValue.get<'FreezedUnionCase.camel' | 'FreezedUnionCase.pascal'>( - 'unionValueCase' - ); - - let atFreezed = '@Freezed(\n'; - - if (copyWith !== undefined) { - atFreezed += indent(`copyWith: ${copyWith},\n`); - } - - if (equal !== undefined) { - atFreezed += indent(`equal: ${equal},\n`); - } - - if (makeCollectionsUnmodifiable !== undefined) { - atFreezed += indent(`makeCollectionsUnmodifiable: ${makeCollectionsUnmodifiable},\n`); - } - - if (unionKey !== undefined) { - atFreezed += indent(`unionKey: ${unionKey},\n`); - } - - if (unionValueCase !== undefined) { - atFreezed += indent(`unionValueCase: '${unionValueCase}',\n`); - } - - atFreezed += ')\n'; - - return atFreezed; - } - // else fallback to the normal @freezed decorator - return '@freezed\n'; - }; - - const isCustomizedFreezed = () => { - return ( - this._freezedConfigValue.get('copyWith') !== undefined || - this._freezedConfigValue.get('equal') !== undefined || - this._freezedConfigValue.get('makeCollectionsUnmodifiable') !== undefined || - this._freezedConfigValue.get('unionKey') !== undefined || - this._freezedConfigValue.get<'FreezedUnionCase.camel' | 'FreezedUnionCase.pascal'>('unionValueCase') !== - undefined - ); - }; - - // this is the start of the pipeline of decisions to determine which Freezed decorator to use - return use_unfreezed(); - } - - private setName(): FreezedDeclarationBlock { - this._name = pascalCase(this._node.name.value); - return this; - } - - private setFactoryBlocks(): FreezedDeclarationBlock { - if (this._node.kind === Kind.UNION_TYPE_DEFINITION) { - this._factoryBlocks = - this._node.types?.map((_type: NamedTypeNode) => new FreezedFactoryBlock(this._config, this._node).init()) ?? []; - } else if (this._node.kind !== Kind.ENUM_TYPE_DEFINITION) { - /* - for `ObjectTypeDefinitionNode` and `InputObjectTypeDefinitionNode` nodes, - we use the `ShapeRepository` - to register the `FreezedFactoryBlock` so that we can use it later - when we are merging inputs or generating freezed union/sealed classes - for GraphQL union types - */ - this._factoryBlocks = - this._node.fields?.map((_field: FieldDefinitionNode | InputValueDefinitionNode) => - this._freezedFactoryBlockRepository.register( - this._node.name.value, - new FreezedFactoryBlock(this._config, this._node).init() - ) - ) ?? []; - } - return this; - } - - private setShape(): FreezedDeclarationBlock { - let shape = ''; - // some helper variables - const name = this._node.name.value; - let namedConstructor: string | undefined; - let factoryBlockKey: string | undefined; - - // handle enums differently - if (this._node.kind === Kind.ENUM_TYPE_DEFINITION) { - this._shape = this._node.values - ?.map((value: EnumValueDefinitionNode) => { - shape = indent(this.getEnumComment(value)); - - if (this._config.camelCasedEnums ?? true) { - shape += `@JsonKey(name: ${value.name.value}) ${value.name.value.toLowerCase()}`; - } else { - shape += value.name.value; - } - return `${shape}\n`; - }) - .join(''); - return this; - } - - // append private empty constructor - if (this._freezedConfigValue.get('privateEmptyConstructor')) { - shape += indent(`const ${this._name}._();\n\n`); - } - - // decide whether to append an empty Union constructor - if (this._freezedConfigValue.get('defaultUnionConstructor') && this._node.kind === Kind.UNION_TYPE_DEFINITION) { - shape += indent(`const factory ${this._name}() = _${this._name};\n\n`); - } - - // append tokens which will be used to retrieve the factory blocks - // from the FreezedFactoryBlockRepository - if (this._node.kind === Kind.UNION_TYPE_DEFINITION) { - this._node?.types?.forEach(type => { - namedConstructor = type.name.value; - factoryBlockKey = namedConstructor; - shape += `==>factory==>${factoryBlockKey}==>${'union_factory'}==>${name}==>${namedConstructor}\n`; - }); - } else { - factoryBlockKey = name; - // replace token for the ObjectType & InputType to be replaced with the default Freezed constructor - shape += `==>factory==>${factoryBlockKey}==>${'class_factory'}==>${name}\n`; - - const mergeInputs = this._freezedConfigValue.get('mergeInputs'); - - if (this._node.kind === Kind.OBJECT_TYPE_DEFINITION && mergeInputs) { - // replace token for the InputTypes(a.k.a namedConstructors) as a union/sealed class - mergeInputs.forEach(input => { - const separator = input.includes('$') ? '$' : input.includes(name) ? name : '*'; - namedConstructor = camelCase(input.split(separator).join('_')); - factoryBlockKey = input.replace('$', name); - shape += `==>factory==>${factoryBlockKey}==>${'merged_input_factory'}==>${name}==>${namedConstructor}\n`; - }); - } - } - - this._shape = shape; - - return this; - } - - /** - * returns the string output of the block - */ - private setBlock(): FreezedDeclarationBlock { let block = ''; - //append comment - block += this._comment; - - // append the decorators - block += this._decorators.join(''); - - // handle enums differently - if (this._node.kind === Kind.ENUM_TYPE_DEFINITION) { - block += `enum ${this._name}{\n${this._shape}}\n\n`; - - this._block = block; - - return this; - } - - // append start of class definition - block += `class ${this._name} with _$${this._name} {\n`; - - // append the shape - block += this._shape; - - // append fromJson - if (this._freezedConfigValue.get('fromJsonToJson')) { - block += indent(`factory ${this._name}.fromJson(Map json) => _${this._name}FromJson(json);\n`); - } - - //append end of class definition - block += '}\n\n'; - - this._block = block; - - return this; - } - - /** returns the block */ - public toString(): string { - if (!this._block) { - throw new Error('setShape must be called before calling toString()'); - } - return this._block; + block += buildBlockComment(node); + block += buildBlockDecorators(config, node); + block += buildBlockHeader(config, node, blockType); + block += buildBlockBody(config, node, blockType); + block += buildBlockFooter(config, node, blockType, blockName); + return block; } } diff --git a/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/factory-block.ts b/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/factory-block.ts index 8dd08674f..1b2e2c47b 100644 --- a/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/factory-block.ts +++ b/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/factory-block.ts @@ -1,170 +1,51 @@ import { indent } from '@graphql-codegen/visitor-plugin-common'; -import { FieldDefinitionNode, InputValueDefinitionNode, Kind } from 'graphql'; -import { camelCase, pascalCase } from 'change-case-all'; -import { FreezedParameterBlock } from './parameter-block.js'; -import { ApplyDecoratorOn, FlutterFreezedPluginConfig } from '../config.js'; -import { FreezedConfigValue, getCustomDecorators, NodeType, transformCustomDecorators } from '../utils.js'; +import { FlutterFreezedPluginConfig } from '../config'; +import { + // buildBlockComment, + // buildBlockDecorators, + buildBlockHeader, + buildBlockBody, + buildBlockFooter, + NodeType, +} from '../utils'; export class FreezedFactoryBlock { - /** document the constructor */ - _comment = ''; - - /** a list of decorators to copy paste to the generator */ - _decorators: string[] = []; - - /** the key of the original type name */ - _key: string | undefined; - - /** the name of the class */ - _name: string | undefined; - - /** the namedConstructor is used for GraphQL Union types or if mergeInput is true */ - _namedConstructor: string | undefined; - - /** a list of interfaces to implements */ - // _implements: string[] = []; - - /** a list of class to mixin with */ - // _mixins: string[] = []; - - /** the parameters of this factory constructor */ - // TODO: handle other parameter types like positional parameters later. - // TODO: sticking to named parameters because GraphQL is a typed language - _parameters: FreezedParameterBlock[] = []; - - /** the shape is the content of the block */ - _shape: string | undefined; - - /** the block is the final structure that is generated */ - _block: string | undefined; - - private _freezedConfigValue: FreezedConfigValue; - - constructor(private _config: FlutterFreezedPluginConfig, private _node: NodeType) { - this._config = _config; - this._node = _node; - this._freezedConfigValue = new FreezedConfigValue(_config, _node.name.value); - } - - public init(): FreezedFactoryBlock { - /* - setDecorators(), setName() and setType() will be called - when the factory is retrieved from the repository - */ - this.setComment().setParameters().setShape().setBlock(); - return this; - } - - private setComment(): FreezedFactoryBlock { - const comment = this._node.description?.value; - - if (comment && comment !== null && comment !== '') { - this._comment = indent(`/// ${comment} \n`); - } - return this; - } - - setDecorators(appliesOn: string, nodeName: string): FreezedFactoryBlock { - this._decorators = [ - ...transformCustomDecorators( - getCustomDecorators(this._config, appliesOn.split(',') as ApplyDecoratorOn[], nodeName), - this._node - ), - ]; - return this; - } - - setKey(key: string): FreezedFactoryBlock { - this._key = pascalCase(key); - return this; - } - - setName(name: string): FreezedFactoryBlock { - this._name = pascalCase(name); - return this; - } - - setNamedConstructor(namedConstructor: string | undefined): FreezedFactoryBlock { - if (namedConstructor) { - this._namedConstructor = camelCase(namedConstructor); - } - return this; - } - - private setParameters(): FreezedFactoryBlock { - // TODO: get this from config directly - const mergeInputs = this._freezedConfigValue.get('mergeInputs'); - const appliesOn: ApplyDecoratorOn[] = this._namedConstructor - ? ['union_factory_parameter'] - : mergeInputs ?? mergeInputs !== [] - ? ['merged_input_parameter'] - : ['class_factory_parameter']; - - if (this._node.kind !== Kind.UNION_TYPE_DEFINITION && this._node.kind !== Kind.ENUM_TYPE_DEFINITION) { - this._parameters = - this._node?.fields?.map((field: FieldDefinitionNode | InputValueDefinitionNode) => - new FreezedParameterBlock(this._config, appliesOn, this._node, field).init() - ) ?? []; - } - return this; - } - - private setShape(): FreezedFactoryBlock { - this._shape = this._parameters.map(p => p.toString()).join(''); - return this; - } - - private setBlock(): FreezedFactoryBlock { + public static build( + config: FlutterFreezedPluginConfig, + node: NodeType, + blockType: 'factory' | 'named_factory', + namedConstructor = '' + ): string { let block = ''; - //append comment - block += this._comment; + // TODO: Implement comments(multi-line) and decoratos - // append the decorators - block += this._decorators.map(d => indent(d)).join(''); - - block += indent(''); - - // decide if to use const or not - if (this._freezedConfigValue.get('immutable')) { - block += 'const '; - } - - // append the factory keyword and the name - block += `factory ${this._name}`; - - // append .namedConstructor is not null - if (this._namedConstructor && this._namedConstructor !== '') { - block += `.${this._namedConstructor}`; - } - - // append the parenthesis for the constructor and braces for the named parameters - block += '({\n'; - - //append the shape - block += this._shape; + // block += buildBlockComment(node); + // block += buildBlockDecorators(node, config); + block += buildBlockHeader(config, node, blockType, undefined, namedConstructor); + block += buildBlockBody(config, node, blockType); + block += buildBlockFooter(config, node, blockType, blockType === 'factory' ? node.name.value : namedConstructor); + return block; + } - // close the constructor and assign the key - block += indent(`}) = `); + public static factoryPlaceholder = (blockName: string): string => { + return indent(`==>factory==>${blockName}\n`); + }; - // but first decide whether prefix the key with an underscore - if (!this._namedConstructor) { - block += '_'; - } + public static namedFactoryPlaceholder = (blockName: string, namedConstructor: string): string => { + return indent(`==>named_factory==>${blockName}==>${namedConstructor}\n`); + }; - // finally, append the key - block += `${this._key};\n`; + public static buildFromFactory = (config: FlutterFreezedPluginConfig, node: NodeType): string => { + return FreezedFactoryBlock.build(config, node, 'factory'); + }; - // store it in the shape - this._block = block; - return this; - } - - /** returns the block */ - public toString(): string { - if (!this._block) { - throw new Error('FreezedFactoryBlock: setShape must be called before calling toString()'); - } - return this._block; - } + public static buildFromNamedFactory = ( + config: FlutterFreezedPluginConfig, + node: NodeType, + blockName: string, + namedConstructor: string + ): string => { + return FreezedFactoryBlock.build(config, node, 'named_factory', namedConstructor); + }; } diff --git a/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/index.ts b/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/index.ts index 160c8dd83..fc3651b67 100644 --- a/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/index.ts +++ b/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/index.ts @@ -1,3 +1,3 @@ -export * from './parameter-block.js'; -export * from './factory-block.js'; -export * from './class-block.js'; +export * from './parameter-block'; +export * from './factory-block'; +export * from './class-block'; diff --git a/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/parameter-block.ts b/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/parameter-block.ts index 2c0a79a2a..4106c69c1 100644 --- a/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/parameter-block.ts +++ b/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/parameter-block.ts @@ -1,206 +1,25 @@ -import { indent } from '@graphql-codegen/visitor-plugin-common'; -import { ListTypeNode, NamedTypeNode, NonNullTypeNode, TypeNode } from 'graphql'; -import { camelCase } from 'change-case-all'; -import { ApplyDecoratorOn, FlutterFreezedPluginConfig } from '../config.js'; -import { getCustomDecorators, transformCustomDecorators, FieldType, FreezedConfigValue, NodeType } from '../utils.js'; - -// TODO: Fix Dart keywords issue - -/** - * maps GraphQL scalar types to Dart's scalar types - */ -export const DART_SCALARS: Record = { - ID: 'String', - String: 'String', - Boolean: 'bool', - Int: 'int', - Float: 'double', - DateTime: 'DateTime', -}; +import { FlutterFreezedPluginConfig } from '../config'; +import { + // buildBlockComment, + // buildBlockDecorators, + buildBlockHeader, + NodeType, + FieldType, +} from '../utils'; export class FreezedParameterBlock { - /** document the property */ - _comment = ''; - - /** a list of decorators to copy paste to the generator */ - _decorators: string[] = []; - - /** mark the property as required */ - _required: boolean | undefined; - - /** mark the property as required */ - // _type?: ParameterType = 'named'; - _type: string | undefined; - - /** the name of the property */ - _name: string | undefined; - - /** the shape is the content of the block */ - _shape: string | undefined; - - /** the block is the final structure that is generated */ - _block: string | undefined; - - private _freezedConfigValue: FreezedConfigValue; - - constructor( - private _config: FlutterFreezedPluginConfig, - private _appliesOn: ApplyDecoratorOn[], - private _node: NodeType, - private _field: FieldType - ) { - this._config = _config; - this._appliesOn = _appliesOn; - this._node = _node; - this._field = _field; - - this._freezedConfigValue = new FreezedConfigValue(_config, _node.name.value); - } - - public init(): FreezedParameterBlock { - this.setComment().setDecorators().setRequired().setType().setName().setShape().setBlock(); - return this; - } - - private setComment(): FreezedParameterBlock { - const comment = this._field.description?.value; - - if (comment && comment !== null && comment !== '') { - this._comment = indent(`/// ${comment}\n`, 2); - } - return this; - } - - private setDecorators(): FreezedParameterBlock { - const nodeName = this._node.name.value; - const fieldName = this._field.name.value; - - // determine if should mark as deprecated - const isDeprecated = this._config.typeSpecificFreezedConfig?.[nodeName]?.fields?.[fieldName]?.deprecated; - const defaultValue = this._config.typeSpecificFreezedConfig?.[nodeName]?.fields?.[fieldName]?.defaultValue; - - if (this._freezedConfigValue.get('alwaysUseJsonKeyName') || fieldName !== camelCase(fieldName)) { - this._decorators = [...this._decorators, `@JsonKey(name: '${fieldName}')\n`]; - } - - this._decorators = [ - ...this._decorators, - ...transformCustomDecorators( - getCustomDecorators(this._config, this._appliesOn, this._node.name.value, fieldName), - this._node, - this._field - ), - ]; - - // @deprecated - // if this._decorators doesn't include an @deprecated decorator but the field is marked as @deprecated... - if (!this._decorators.includes('@deprecated') && isDeprecated) { - this._decorators = [...this._decorators, '@deprecated\n']; - } - - // @Default - if (defaultValue) { - //overwrite the customDecorator's defaultValue - this._decorators = this._decorators.filter(d => !d.startsWith('@Default')); - this._decorators = [...this._decorators, `@Default(value: ${defaultValue})\n`]; - } - return this; - } - - private setRequired(): FreezedParameterBlock { - this._required = this.isNonNullType(this._field.type); - return this; - } - - private setType(): FreezedParameterBlock { - this._type = this.propertyType(this._field, this._field.type); - return this; - } - - private setName(): FreezedParameterBlock { - this._name = camelCase(this._field.name.value); - return this; - } - - /** compose the freezed constructor property */ - private setShape(): FreezedParameterBlock { - let shape = ''; - const nodeName = this._node.name.value; - const fieldName = this._field.name.value; - - // determine if should mark as final - const isFinal = - this._decorators.includes('final') || - this._config.typeSpecificFreezedConfig?.[nodeName]?.fields?.[fieldName]?.final; - - //append comment - shape += this._comment; - - // append the decorators - shape += this._decorators - .filter(d => d !== 'final') - .map(d => indent(d, 2)) - .join(''); - - // append required for non-nullable types - shape += indent(this._required ? 'required ' : '', 2); - - // append isFinal - shape += isFinal ? 'final ' : ''; - - // append the Dart Type, name and trailing comma - shape += `${this._type} ${this._name},\n`; - - // store it in the shape - this._shape = shape; - - return this; - } - - /** composes the full block */ - private setBlock(): FreezedParameterBlock { - this._block = this._shape; - return this; - } - - private propertyType = (field: FieldType, type: TypeNode, parentType?: TypeNode): string => { - if (this.isNonNullType(type)) { - return this.propertyType(field, type.type, type); - } - - if (this.isListType(type)) { - const T = this.propertyType(field, type.type, type); - return `List<${T}>${this.isNonNullType(parentType) ? '' : '?'}`; - } - - if (this.isNamedType(type)) { - return `${this.scalar(type.name.value)}${this.isNonNullType(parentType) ? '' : '?'}`; - } - - return ''; - }; - - private isListType = (type?: TypeNode): type is ListTypeNode => type?.kind === 'ListType'; - - private isNonNullType = (type?: TypeNode): type is NonNullTypeNode => type?.kind === 'NonNullType'; - - private isNamedType = (type?: TypeNode): type is NamedTypeNode => type?.kind === 'NamedType'; - - private scalar(_scalar: string): string { - if (this._config?.customScalars?.[_scalar]) { - return this._config.customScalars[_scalar]; - } - if (DART_SCALARS[_scalar]) { - return DART_SCALARS[_scalar]; - } - return _scalar; - } - - /** returns the block */ - public toString(): string { - if (!this._block) { - throw new Error('FreezedParameterBlock: setShape must be called before calling toString()'); - } - return this._block; + public static build( + config: FlutterFreezedPluginConfig, + node: NodeType, + blockType: 'parameter', + field: FieldType + ): string { + let block = ''; + + // TODO: Implement comments(multi-line) and decoratos + // block += buildBlockComment(node); + // block += buildBlockDecorators(node, config); + block += buildBlockHeader(config, node, blockType, field); + return block; } } diff --git a/packages/plugins/dart/flutter-freezed/src/index.ts b/packages/plugins/dart/flutter-freezed/src/index.ts index 84a05d430..c7469c69d 100644 --- a/packages/plugins/dart/flutter-freezed/src/index.ts +++ b/packages/plugins/dart/flutter-freezed/src/index.ts @@ -1,38 +1,39 @@ import { oldVisit, PluginFunction, Types } from '@graphql-codegen/plugin-helpers'; import { transformSchemaAST } from '@graphql-codegen/schema-ast'; import { GraphQLSchema } from 'graphql'; -import { FlutterFreezedPluginConfig } from './config.js'; -import { FreezedDeclarationBlock } from './freezed-declaration-blocks/index.js'; -import { schemaVisitor } from './schema-visitor.js'; -import { addFreezedImportStatements, DefaultFreezedPluginConfig } from './utils.js'; +import { FlutterFreezedPluginConfig } from './config'; +import { schemaVisitor } from './schema-visitor'; +import { buildImportStatements, defaultFreezedPluginConfig } from './utils'; export const plugin: PluginFunction = ( schema: GraphQLSchema, _documents: Types.DocumentFile[], - config: FlutterFreezedPluginConfig + _config: FlutterFreezedPluginConfig ): string => { // sets the defaults for the config - config = { ...new DefaultFreezedPluginConfig(config) }; + const config = { ...defaultFreezedPluginConfig, ..._config }; const { schema: _schema, ast } = transformSchemaAST(schema, config); - const { freezedFactoryBlockRepository, ...visitor } = schemaVisitor(_schema, config); + const { nodeRepository, ...visitor } = schemaVisitor(_schema, config); const visitorResult = oldVisit(ast, { leave: visitor }); - const generated: FreezedDeclarationBlock[] = visitorResult.definitions.filter( - (def: any) => def instanceof FreezedDeclarationBlock - ); + const generated: string[] = visitorResult.definitions.filter((def: any) => typeof def === 'string' && def.length > 0); return ( - addFreezedImportStatements(config.fileName) + - generated - .map(freezedDeclarationBlock => + buildImportStatements(config.fileName) + + generated // TODO: replace placeholders with factory blocks + /* .map(freezedDeclarationBlock => freezedDeclarationBlock.toString().replace(/==>factory==>.+\n/gm, s => { const pattern = s.replace('==>factory==>', '').trim(); + // console.log('pattern:-->', pattern); const [key, appliesOn, name, typeName] = pattern.split('==>'); - return freezedFactoryBlockRepository.retrieve(key, appliesOn, name, typeName ?? null); + if (appliesOn === 'class_factory') { + return freezedFactoryBlockRepository.retrieve(key, appliesOn, name); + } + return freezedFactoryBlockRepository.retrieve(key, appliesOn, name, typeName); }) - ) + ) */ .join('') .trim() ); diff --git a/packages/plugins/dart/flutter-freezed/src/schema-visitor.ts b/packages/plugins/dart/flutter-freezed/src/schema-visitor.ts index 4f9d1ae91..f5abf57bc 100644 --- a/packages/plugins/dart/flutter-freezed/src/schema-visitor.ts +++ b/packages/plugins/dart/flutter-freezed/src/schema-visitor.ts @@ -5,24 +5,20 @@ import { ObjectTypeDefinitionNode, UnionTypeDefinitionNode, } from 'graphql'; -import { FlutterFreezedPluginConfig } from './config.js'; -import { FreezedFactoryBlockRepository, transformDefinition } from './utils.js'; +import { FlutterFreezedPluginConfig } from './config'; +import { buildBlock, NodeRepository } from './utils'; export const schemaVisitor = (_schema: GraphQLSchema, config: FlutterFreezedPluginConfig) => { - const freezedFactoryBlockRepository = new FreezedFactoryBlockRepository(); + const nodeRepository = new NodeRepository(); return { - freezedFactoryBlockRepository, + nodeRepository, - EnumTypeDefinition: (node: EnumTypeDefinitionNode) => - transformDefinition(config, freezedFactoryBlockRepository, node), + EnumTypeDefinition: (node: EnumTypeDefinitionNode) => buildBlock(config, node, nodeRepository), - UnionTypeDefinition: (node: UnionTypeDefinitionNode) => - transformDefinition(config, freezedFactoryBlockRepository, node), + UnionTypeDefinition: (node: UnionTypeDefinitionNode) => buildBlock(config, node, nodeRepository), - ObjectTypeDefinition: (node: ObjectTypeDefinitionNode) => - transformDefinition(config, freezedFactoryBlockRepository, node), + ObjectTypeDefinition: (node: ObjectTypeDefinitionNode) => buildBlock(config, node, nodeRepository), - InputObjectTypeDefinition: (node: InputObjectTypeDefinitionNode) => - transformDefinition(config, freezedFactoryBlockRepository, node), + InputObjectTypeDefinition: (node: InputObjectTypeDefinitionNode) => buildBlock(config, node, nodeRepository), }; }; diff --git a/packages/plugins/dart/flutter-freezed/src/utils.ts b/packages/plugins/dart/flutter-freezed/src/utils.ts index 84105173d..70982d69a 100644 --- a/packages/plugins/dart/flutter-freezed/src/utils.ts +++ b/packages/plugins/dart/flutter-freezed/src/utils.ts @@ -1,11 +1,20 @@ +import { indent } from '@graphql-codegen/visitor-plugin-common'; +import { camelCase, pascalCase, snakeCase } from 'change-case-all'; import { ArgumentNode, + DefinitionNode, DirectiveNode, EnumTypeDefinitionNode, + EnumValueDefinitionNode, FieldDefinitionNode, InputObjectTypeDefinitionNode, InputValueDefinitionNode, + Kind, + ListTypeNode, + NamedTypeNode, + NonNullTypeNode, ObjectTypeDefinitionNode, + TypeNode, UnionTypeDefinitionNode, } from 'graphql'; import { @@ -13,11 +22,14 @@ import { CustomDecorator, FreezedConfig, FlutterFreezedPluginConfig, - TypeSpecificFreezedConfig, -} from './config.js'; -import { FreezedDeclarationBlock, FreezedFactoryBlock } from './freezed-declaration-blocks/index.js'; + DART_SCALARS, + DartIdentifierCasing, + DART_KEYWORDS, +} from './config'; +import { FreezedDeclarationBlock, FreezedFactoryBlock } from './freezed-declaration-blocks'; +import { FreezedParameterBlock } from './freezed-declaration-blocks/parameter-block'; -export type FieldType = FieldDefinitionNode | InputValueDefinitionNode; +export type FreezedConfigOptionName = keyof FreezedConfig; export type NodeType = | ObjectTypeDefinitionNode @@ -25,72 +37,349 @@ export type NodeType = | UnionTypeDefinitionNode | EnumTypeDefinitionNode; -export type OptionName = - // FreezedClassConfig - | 'alwaysUseJsonKeyName' - | 'copyWith' - | 'customDecorators' - | 'defaultUnionConstructor' - | 'equal' - | 'fromJsonToJson' - | 'immutable' - | 'makeCollectionsUnmodifiable' - | 'mergeInputs' - | 'mutableInputs' - | 'privateEmptyConstructor' - | 'unionKey' - | 'unionValueCase'; - -export function transformDefinition( - config: FlutterFreezedPluginConfig, - freezedFactoryBlockRepository: FreezedFactoryBlockRepository, - node: NodeType -) { - // ignore these... - if (['Query', 'Mutation', 'Subscription', ...(config?.ignoreTypes ?? [])].includes(node.name.value)) { - return ''; - } +export type FieldType = FieldDefinitionNode | InputValueDefinitionNode; - return new FreezedDeclarationBlock(config, freezedFactoryBlockRepository, node).init(); -} +export type ObjectType = ObjectTypeDefinitionNode | InputObjectTypeDefinitionNode; + +/** initializes a FreezedConfig with the defaults values */ +export const defaultFreezedConfig: FreezedConfig = { + alwaysUseJsonKeyName: false, + copyWith: undefined, + customDecorators: {}, + dartKeywordEscapeCasing: undefined, + dartKeywordEscapePrefix: undefined, + dartKeywordEscapeSuffix: '_', + equal: undefined, + escapeDartKeywords: true, + fromJsonToJson: true, + immutable: true, + makeCollectionsUnmodifiable: undefined, + mergeInputs: [], + mutableInputs: true, + privateEmptyConstructor: true, + unionKey: undefined, + unionValueCase: undefined, +}; + +/** initializes a FreezedPluginConfig with the defaults values */ +export const defaultFreezedPluginConfig: FlutterFreezedPluginConfig = { + camelCasedEnums: true, + customScalars: {}, + fileName: 'app_models', + globalFreezedConfig: { ...defaultFreezedConfig }, + ignoreTypes: [], + typeSpecificFreezedConfig: {}, +}; + +export const mergeConfig = ( + baseConfig?: Partial, + newConfig?: Partial +): FlutterFreezedPluginConfig => { + return { + camelCasedEnums: newConfig?.camelCasedEnums ?? baseConfig?.camelCasedEnums ?? true, + customScalars: { ...(baseConfig?.customScalars ?? {}), ...(newConfig?.customScalars ?? {}) }, + fileName: newConfig?.fileName ?? baseConfig?.fileName ?? 'app_models', + globalFreezedConfig: { + ...defaultFreezedConfig, + ...(baseConfig?.globalFreezedConfig ?? {}), + ...(newConfig?.globalFreezedConfig ?? {}), + }, + ignoreTypes: [...(baseConfig?.ignoreTypes ?? []), ...(newConfig?.ignoreTypes ?? [])], + typeSpecificFreezedConfig: { + ...(baseConfig?.typeSpecificFreezedConfig ?? {}), + ...(newConfig?.typeSpecificFreezedConfig ?? {}), + }, + }; +}; + +//#region helpers + +export const nodeIsObjectType = ( + node: DefinitionNode +): node is ObjectTypeDefinitionNode | InputObjectTypeDefinitionNode => + node.kind === Kind.OBJECT_TYPE_DEFINITION || node.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION; /** - * returns the value of the FreezedConfig option - * for a specific type if given typeName - * or else fallback to the global FreezedConfig value + * Returns the value of the `FreezedConfig` option for a specific type if given `typeName` or else fallbacks to the value of the `globalFreezedConfig` for that option + * @param option The name of the `FreezedConfig` config option + * @param config The plugin configuration object + * @param typeName The Graphql Type name, used to get the config option from `typeSpecificFreezedConfig` + * @param defaultValue If the value of the config option is undefined, this default value will be returned + * @returns The value of the config option */ -export function getFreezedConfigValue( - option: OptionName, +export function getFreezedConfigValue( + option: FreezedConfigOptionName, config: FlutterFreezedPluginConfig, - typeName?: string | undefined -): any { + typeName?: string, + defaultValue?: T +): T { if (typeName) { - return config?.typeSpecificFreezedConfig?.[typeName]?.config?.[option] ?? getFreezedConfigValue(option, config); + return (config?.typeSpecificFreezedConfig?.[typeName]?.config?.[option] ?? + getFreezedConfigValue(option, config, undefined, defaultValue)) as T; } - return config?.globalFreezedConfig?.[option]; + return (config?.globalFreezedConfig?.[option] ?? defaultValue) as T; } +/** + * Returns a string of import statements placed at the top of the file that contains generated models + * @param fileName The name of the file where the generated Freezed models will be saved to. This is used to import the library part files generated by Freezed. This value must be set in the plugin's config + * @returns a string of import statements + */ +export const buildImportStatements = (fileName: string) => { + if (fileName.length < 1) { + throw new Error('fileName is required and must not be empty'); + } + const segments = fileName.split('/'); + const target = segments[segments.length - 1]; + const expectedFileName = snakeCase(target.replace(/\.dart/g, '')); + return [ + `import 'package:freezed_annotation/freezed_annotation.dart';\n`, + `import 'package:flutter/foundation.dart';\n\n`, + `part '${expectedFileName}.freezed.dart';\n`, + `part '${expectedFileName}.g.dart';\n\n`, + ].join(''); +}; + +/** + * constructs the name for Freezed enum, class, factory and parameter blocks + * @param config The plugin configuration object + * @param blockName The name of the block usually derived from the `node.name.value`, `field.name.value` or `type.name.value` when building Freezed block + * @param typeName The Graphql Type name, used to get the config option from `typeSpecificFreezedConfig` + * @param casing Specify the casing to be used for the name. Available options are: `snake_case`, `camelCase`, `PascalCase` + * @param decorateWithAtJsonKey wraps the name in a `@JsonKey(name: 'blockName') name` decorator + * @returns the new camelCased/PascalCased `@JsonKey` decorated name for the FreezedBlock + */ +export const buildBlockName = ( + config: FlutterFreezedPluginConfig, + blockName: string, + typeName: string = blockName, + casing?: DartIdentifierCasing, + decorateWithAtJsonKey?: boolean +): string => { + const escapedBlockName = escapeDartKeyword(config, blockName, typeName); + + const casedBlockName = dartCasing(escapedBlockName, casing); + + if (isDartKeyword(casedBlockName)) { + const escapedBlockName = escapeDartKeyword(config, casedBlockName, typeName); + return decorateWithAtJsonKey ? `@JsonKey(name: '${blockName}') ${escapedBlockName}` : escapedBlockName; + } + return decorateWithAtJsonKey ? `@JsonKey(name: '${blockName}') ${casedBlockName}` : casedBlockName; +}; + +/** + * checks whether name is a Dart Language keyword + * @param name The name or identifier to be checked + * @returns `true` if name is a Dart Language keyword, otherwise `false` + */ +export const isDartKeyword = (name: string) => Object.hasOwn(DART_KEYWORDS, name); + +/** + * Ensures that the `blockName` isn't a valid Dart language reserved keyword. It wraps the `blockName` the `dartKeywordEscapePrefix`, `dartKeywordEscapeSuffix` and `dartKeywordEscapeCasing` specified in the config + * @param config The plugin configuration object + * @param blockName The name of the block usually derived from the `node.name.value`, `field.name.value` or `type.name.value` when building Freezed block + * @param typeName The Graphql Type name, used to get the config option from `typeSpecificFreezedConfig` + * @returns + */ +export const escapeDartKeyword = (config: FlutterFreezedPluginConfig, blockName: string, typeName?: string): string => { + if (isDartKeyword(blockName)) { + const prefix = getFreezedConfigValue('dartKeywordEscapePrefix', config, typeName, ''); + const suffix = getFreezedConfigValue('dartKeywordEscapeSuffix', config, typeName, ''); + const casing = getFreezedConfigValue('dartKeywordEscapeCasing', config, typeName); + + const escapedBlockName = `${prefix}${blockName}${suffix}`; + + return dartCasing(escapedBlockName, casing); + } + return blockName; +}; + +export const dartCasing = (name: string, casing?: DartIdentifierCasing): string => { + if (casing === 'camelCase') { + return camelCase(name); + } else if (casing === 'PascalCase') { + return pascalCase(name); + } else if (casing === 'snake_case') { + return snakeCase(name); + } + return name; +}; + +export const shouldDecorateWithAtJsonKey = ( + blockType: 'enum_field' | 'parameter_field', + config: FlutterFreezedPluginConfig, + blockName: string, + typeName: string +): boolean => { + const alwaysUseJsonKeyName = getFreezedConfigValue('alwaysUseJsonKeyName', config, typeName, false); + const alreadyCamelCased = !isDartKeyword(blockName) && camelCase(blockName) === blockName; + + if (alwaysUseJsonKeyName) { + return true; + } else if (alreadyCamelCased) { + return false; + } else if (blockType === 'enum_field') { + return config.camelCasedEnums ?? true; + } + return false; +}; +//#endregion + +//#region Step 01. Start Here +/** + * Transforms the AST nodes into Freezed classes/models + * @param config The plugin configuration object + * @param node the AST node passed by the schema visitor + * @param nodeRepository A map that stores the name of the Graphql Type as the key and it AST node as the value. Used to build FactoryBlocks from placeholders for mergedInputs and Union Types + * @returns a string output of a `FreezedDeclarationBlock` which represents a Freezed class/model in Dart + */ +export const buildBlock = (config: FlutterFreezedPluginConfig, node: NodeType, nodeRepository: NodeRepository) => { + // ignore these... + if (['Query', 'Mutation', 'Subscription', ...(config?.ignoreTypes ?? [])].includes(node.name.value)) { + return ''; + } + + // registers all the ObjectTypes + if (nodeIsObjectType(node)) { + nodeRepository.register(node); + } + + return FreezedDeclarationBlock.build(config, node); +}; + +//#endregion + +//#region Step 02. Build Comments + +// TODO: handle multiline comment +// TODO: Change param `node` to string for easy testing +export const buildBlockComment = (node?: NodeType | EnumValueDefinitionNode): string => { + const comment = node?.description?.value; + + return comment && comment?.length > 0 ? `/// ${comment} \n` : ''; +}; + +//#endregion + +//#region Step 03. Build Decorators + +// TODO: modify this for factory blocks too +export const buildBlockDecorators = (config: FlutterFreezedPluginConfig, node: NodeType): string => { + const name = node.name.value; + + // determine if should mark as deprecated + const isDeprecated = config.typeSpecificFreezedConfig?.[name]?.deprecated; + + const decorators = + node.kind === Kind.ENUM_TYPE_DEFINITION + ? [...transformCustomDecorators(getCustomDecorators(config, ['enum'], name), node)] + : [ + buildFreezedDecorator(config, node), + ...transformCustomDecorators(getCustomDecorators(config, ['class'], name), node), + ]; + + return decorators + .filter(d => d !== '@deprecated') + .concat(isDeprecated ? ['@deprecated\n'] : []) + .join(''); +}; + +export const buildFreezedDecorator = (config: FlutterFreezedPluginConfig, node: NodeType) => { + // this is the start of the pipeline of decisions to determine which Freezed decorator to use + return decorateAsUnfreezed(config, node); +}; + +export const decorateAsUnfreezed = (config: FlutterFreezedPluginConfig, node: NodeType) => { + const typeName = node.name.value; + + const mutable = + !getFreezedConfigValue('immutable', config, typeName) || + (node.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION && getFreezedConfigValue('mutableInputs', config, typeName)); + + return mutable ? '@unfreezed\n' : decorateAsFreezed(config, node); +}; + +export const decorateAsFreezed = (config: FlutterFreezedPluginConfig, node: NodeType) => { + const typeName = node.name.value; + + if (isCustomizedFreezed(config, node)) { + const copyWith = getFreezedConfigValue('copyWith', config, typeName); + const equal = getFreezedConfigValue('equal', config, typeName); + const makeCollectionsUnmodifiable = getFreezedConfigValue('makeCollectionsUnmodifiable', config, typeName); + const unionKey = getFreezedConfigValue('unionKey', config, typeName); + const unionValueCase = getFreezedConfigValue<'FreezedUnionCase.camel' | 'FreezedUnionCase.pascal'>( + 'unionValueCase', + config, + typeName + ); + + let atFreezed = '@Freezed(\n'; + + if (copyWith !== undefined) { + atFreezed += indent(`copyWith: ${copyWith},\n`); + } + + if (equal !== undefined) { + atFreezed += indent(`equal: ${equal},\n`); + } + + if (makeCollectionsUnmodifiable !== undefined) { + atFreezed += indent(`makeCollectionsUnmodifiable: ${makeCollectionsUnmodifiable},\n`); + } + + if (unionKey !== undefined) { + atFreezed += indent(`unionKey: ${unionKey},\n`); + } + + if (unionValueCase !== undefined) { + atFreezed += indent(`unionValueCase: '${unionValueCase}',\n`); + } + + atFreezed += ')\n'; + + return atFreezed; + } + // else fallback to the normal `@freezed` decorator + return '@freezed\n'; +}; + +export const isCustomizedFreezed = (config: FlutterFreezedPluginConfig, node: NodeType) => { + const typeName = node.name.value; + + return ( + getFreezedConfigValue('copyWith', config, typeName) !== undefined || + getFreezedConfigValue('equal', config, typeName) !== undefined || + getFreezedConfigValue('makeCollectionsUnmodifiable', config, typeName) !== undefined || + getFreezedConfigValue('unionKey', config, typeName) !== undefined || + getFreezedConfigValue<'FreezedUnionCase.camel' | 'FreezedUnionCase.pascal'>('unionValueCase', config, typeName) !== + undefined + ); +}; + /** * @description filters the customDirectives to return those that are applied on a list of blocks */ export function getCustomDecorators( config: FlutterFreezedPluginConfig, appliesOn: ApplyDecoratorOn[], - nodeName?: string | undefined, + typeName?: string | undefined, fieldName?: string | undefined ): CustomDecorator { const filteredCustomDecorators: CustomDecorator = {}; const globalCustomDecorators = config?.globalFreezedConfig?.customDecorators ?? {}; let customDecorators: CustomDecorator = { ...globalCustomDecorators }; - if (nodeName) { - const typeConfig = config?.typeSpecificFreezedConfig?.[nodeName]; + if (typeName) { + const typeConfig = config?.typeSpecificFreezedConfig?.[typeName]; const typeSpecificCustomDecorators = typeConfig?.config?.customDecorators ?? {}; customDecorators = { ...customDecorators, ...typeSpecificCustomDecorators }; if (fieldName) { const fieldSpecificCustomDecorators = typeConfig?.fields?.[fieldName]?.customDecorators ?? {}; - customDecorators = { ...customDecorators, ...fieldSpecificCustomDecorators }; + customDecorators = { + ...customDecorators, + ...fieldSpecificCustomDecorators, + }; } } @@ -135,7 +424,7 @@ export function transformCustomDecorators( if (value.mapsToFreezedAs === 'custom') { const args = value?.arguments; // if the custom directives have arguments, - if (args && args !== []) { + if (args && args.length > 0) { // join them with a comma in the parenthesis result = [...result, `${key}(${args.join(', ')})\n`]; } else { @@ -169,7 +458,7 @@ function directiveToString(directive: DirectiveNode, customDecorators: CustomDec .map(a => `${a.name}: ${a.value}`); // if the args is not empty - if (args !== []) { + if (args && args.length > 0) { // returns "@directiveName(argName: argValue, argName: argValue ...)" return `@${directive.name.value}(${args?.join(', ')})\n`; } @@ -190,122 +479,259 @@ function argToInt(arg: string) { return parsedIndex ? parsedIndex : 0; } -/** returns freezed import statements */ -export function addFreezedImportStatements(fileName: string) { - return [ - "import 'package:freezed_annotation/freezed_annotation.dart';\n", - "import 'package:flutter/foundation.dart';\n\n", - `part ${fileName.replace(/\.dart/g, '')}.dart;\n`, - `part '${fileName.replace(/\.dart/g, '')}.g.dart';\n\n`, - ].join(''); -} +//#endregion -/** a class variant of the getFreezedConfigValue helper function - * - * returns the value of the FreezedConfig option - * for a specific type if given typeName - * or else fallback to the global FreezedConfig value - */ -export class FreezedConfigValue { - constructor(private _config: FlutterFreezedPluginConfig, private _typeName: string | undefined) { - this._config = _config; - this._typeName = _typeName; +//#region Step 03. Build Blocks + +export const buildBlockHeader = ( + config: FlutterFreezedPluginConfig, + node: NodeType, + blockType: 'enum' | 'class' | 'factory' | 'named_factory' | 'parameter', + field?: FieldType, + namedConstructor = '' +): string => { + const blockName = node.name.value; + const typeName = blockName; + + if (blockType === 'enum') { + return buildEnumHeader(config, blockName); + } else if (blockType === 'class') { + const withPrivateEmptyConstructor = getFreezedConfigValue('privateEmptyConstructor', config, typeName); + return buildClassHeader(config, blockName, withPrivateEmptyConstructor); + } else if (blockType === 'factory' || blockType === 'named_factory') { + const immutable = getFreezedConfigValue('immutable', config, typeName); + return buildFactoryHeader(config, blockName, namedConstructor, immutable); + } else if (blockType === 'parameter' && field) { + return buildParameterHeader(config, node, field); } + return ''; +}; - /** - * returns the value of the FreezedConfig option - * for a specific type if given typeName - * or else fallback to the global FreezedConfig value - */ - get(option: OptionName): T { - return getFreezedConfigValue(option, this._config, this._typeName) as T; +export const buildBlockBody = ( + config: FlutterFreezedPluginConfig, + node: NodeType, + blockType: 'enum' | 'class' | 'factory' | 'named_factory' +): string => { + if (blockType === 'enum' && node.kind === Kind.ENUM_TYPE_DEFINITION) { + return buildEnumBody(config, node); + } else if (blockType === 'class') { + return buildClassBody(config, node); + } else if ((blockType === 'factory' || blockType === 'named_factory') && nodeIsObjectType(node)) { + return buildFactoryBody(config, node); } -} + return ''; +}; -/** - * stores an instance of FreezedFactoryBlock using the node names as the key - * and returns that instance when replacing tokens - * */ -export class FreezedFactoryBlockRepository { - _store: Record = {}; +export const buildBlockFooter = ( + config: FlutterFreezedPluginConfig, + node: NodeType | FieldType, + blockType: 'enum' | 'class' | 'factory' | 'named_factory', + namedConstructor = '' +): string => { + const blockName = node.name.value; + const typeName = blockName; - get(key: string): FreezedFactoryBlock | undefined { - return this._store[key]; + if (blockType === 'enum') { + return buildEnumFooter(); + } else if (blockType === 'class') { + const fromJsonToJson = getFreezedConfigValue('fromJsonToJson', config, typeName); + return buildClassFooter(config, blockName, fromJsonToJson); + } else if (blockType === 'factory' || (blockType === 'named_factory' && namedConstructor.length > 0)) { + return buildFactoryFooter(config, blockType, namedConstructor); } + return ''; +}; +//#endregion + +//#region Step 03.01. Build Enum Block + +export const buildEnumHeader = (config: FlutterFreezedPluginConfig, blockName: string): string => { + return `enum ${buildBlockName(config, blockName)} {\n`; +}; + +export const buildEnumBody = (config: FlutterFreezedPluginConfig, node: EnumTypeDefinitionNode): string => { + return ( + node.values + ?.map((enumValue: EnumValueDefinitionNode) => { + const blockName = enumValue.name.value; + const typeName = node.name.value; + const camelCased = config.camelCasedEnums ?? true; + const casing: DartIdentifierCasing | undefined = camelCased ? 'camelCase' : undefined; + const decorateWithAtJsonKey = shouldDecorateWithAtJsonKey('enum_field', config, blockName, typeName); + const enumField = buildBlockName(config, blockName, typeName, casing, decorateWithAtJsonKey); + + return indent(`${buildBlockComment(enumValue)}${enumField},\n`); + }) + .join('') ?? '' + ); +}; + +export const buildEnumFooter = (): string => { + return '}\n\n'; +}; + +//#endregion + +//#region Step 03.02. Build Class Block - register(key: string, value: FreezedFactoryBlock): FreezedFactoryBlock { - this._store[key] = value; - return value; +export const buildClassHeader = ( + config: FlutterFreezedPluginConfig, + blockName: string, + withPrivateEmptyConstructor = true +): string => { + const typeName = blockName; + const className = buildBlockName(config, blockName, typeName, 'PascalCase', false); + + const privateEmptyConstructor = withPrivateEmptyConstructor ? indent(`const ${className}._();\n\n`) : ''; + + return `class ${className} with _$${className} {\n${privateEmptyConstructor}`; +}; + +export const buildClassBody = (config: FlutterFreezedPluginConfig, node: NodeType): string => { + const blockName = node.name.value; + + if (node.kind === Kind.UNION_TYPE_DEFINITION) { + return ( + node.types?.map(value => FreezedFactoryBlock.namedFactoryPlaceholder(blockName, value.name.value)).join('') ?? '' + ); + } else if (nodeIsObjectType(node)) { + return FreezedFactoryBlock.factoryPlaceholder(blockName); + // TODO: Determine whether to mergeInputs } + return ''; +}; - retrieve(key: string, appliesOn: string, name: string, typeName: string | undefined): string { - if (this._store[key]) { - return ( - this._store[key] - .setDecorators(appliesOn, key) - .setKey(key) - .setName(name) - .setNamedConstructor(typeName) - .init() - .toString() + '\n' - ); - } - return ''; +export const buildClassFooter = ( + config: FlutterFreezedPluginConfig, + blockName: string, + fromJsonToJson = true +): string => { + const typeName = blockName; + const className = buildBlockName(config, blockName, typeName, 'PascalCase', false); + + if (fromJsonToJson) { + return indent(`factory ${className}.fromJson(Map json) => _$${className}FromJson(json);\n}\n\n`); } -} + return '}\n\n'; +}; -/** initializes a FreezedPluginConfig with the defaults values */ -export class DefaultFreezedPluginConfig implements FlutterFreezedPluginConfig { - camelCasedEnums?: boolean; - customScalars?: { [name: string]: string }; - fileName?: string; - globalFreezedConfig?: FreezedConfig; - typeSpecificFreezedConfig?: Record; - ignoreTypes?: string[]; - - constructor(config: FlutterFreezedPluginConfig = {}) { - Object.assign(this, { - camelCasedEnums: config.camelCasedEnums ?? true, - customScalars: config.customScalars ?? {}, - fileName: config.fileName ?? 'app_models', - globalFreezedConfig: { ...new DefaultFreezedConfig(), ...(config.globalFreezedConfig ?? {}) }, - typeSpecificFreezedConfig: config.typeSpecificFreezedConfig ?? {}, - ignoreTypes: config.ignoreTypes ?? [], - }); +//#endregion + +//#region Step 03.03. Build Factory Block + +export const buildFactoryHeader = ( + config: FlutterFreezedPluginConfig, + blockName: string, + namedConstructor = '', + immutable = true +) => { + const constFactory = immutable ? 'const factory' : 'factory'; + const typeName = blockName; + const decorateWithAtJsonKey = false; + const escapedBlockName = buildBlockName(config, blockName, typeName, 'PascalCase', decorateWithAtJsonKey); + + if (namedConstructor.length > 0) { + const blockName = namedConstructor; + const typeName = namedConstructor; + const decorateWithAtJsonKey = false; + const escapedNamedConstructor = buildBlockName(config, blockName, typeName, 'camelCase', decorateWithAtJsonKey); + + return `${constFactory} ${escapedBlockName}.${escapedNamedConstructor}({\n`; } -} -/** initializes a FreezedConfig with the defaults values */ -export class DefaultFreezedConfig implements FreezedConfig { - alwaysUseJsonKeyName?: boolean; - copyWith?: boolean; - customDecorators?: CustomDecorator; - defaultUnionConstructor?: boolean; - equal?: boolean; - fromJsonToJson?: boolean; - immutable?: boolean; - makeCollectionsUnmodifiable?: boolean; - mergeInputs?: string[]; - mutableInputs?: boolean; - privateEmptyConstructor?: boolean; - unionKey?: string; - unionValueCase?: 'FreezedUnionCase.camel' | 'FreezedUnionCase.pascal'; - - constructor() { - Object.assign(this, { - alwaysUseJsonKeyName: false, - copyWith: undefined, - customDecorators: {}, - defaultUnionConstructor: true, - equal: undefined, - fromJsonToJson: true, - immutable: true, - makeCollectionsUnmodifiable: undefined, - mergeInputs: [], - mutableInputs: true, - privateEmptyConstructor: true, - unionKey: undefined, - unionValueCase: undefined, - }); + return `${constFactory} ${escapedBlockName}({\n`; +}; + +export const buildFactoryBody = (config: FlutterFreezedPluginConfig, node: ObjectType): string => { + return node.fields?.map(field => FreezedParameterBlock.build(config, node, 'parameter', field)).join('') ?? ''; +}; + +export const buildFactoryFooter = ( + config: FlutterFreezedPluginConfig, + blockType: 'factory' | 'named_factory', + namedConstructor: string +) => { + const blockName = namedConstructor; + const typeName = namedConstructor; + const decorateWithAtJsonKey = false; + const prefix = blockType === 'factory' ? '_' : ''; + const factoryFooterName = buildBlockName(config, blockName, typeName, 'PascalCase', decorateWithAtJsonKey); + return `}) = ${prefix}${factoryFooterName};\n`; +}; + +//#endregion + +//#region Step 03.04. Build Parameter Block + +export const buildParameterHeader = (config: FlutterFreezedPluginConfig, node: NodeType, field: FieldType): string => { + const decorators = ''; // TODO: modify the buildBlockDecorators to be used here + + const markedFinal = + decorators.includes('final') || + config.typeSpecificFreezedConfig?.[node.name.value]?.fields?.[field.name.value]?.final; + + const required = isNonNullType(field.type) ? 'required ' : ''; + const final = markedFinal ? 'final ' : ''; + const type = parameterType(config, field.type); + const blockName = field.name.value; + const typeName = node.name.value; + const decorateWithAtJsonKey = shouldDecorateWithAtJsonKey('parameter_field', config, blockName, typeName); + const name = buildBlockName(config, blockName, typeName, 'camelCase', decorateWithAtJsonKey); + + return indent(`${required}${final} ${type} ${name},\n`, 2); +}; + +export const parameterType = (config: FlutterFreezedPluginConfig, type: TypeNode, parentType?: TypeNode): string => { + if (isNonNullType(type)) { + return parameterType(config, type.type, type); + } + + if (isListType(type)) { + const T = parameterType(config, type.type, type); + return `List<${T}>${isNonNullType(parentType) ? '' : '?'}`; + } + + if (isNamedType(type)) { + return `${getScalarType(config, type.name.value)}${isNonNullType(parentType) ? '' : '?'}`; + } + + return ''; +}; + +export const isListType = (type?: TypeNode): type is ListTypeNode => type?.kind === 'ListType'; + +export const isNonNullType = (type?: TypeNode): type is NonNullTypeNode => type?.kind === 'NonNullType'; + +export const isNamedType = (type?: TypeNode): type is NamedTypeNode => type?.kind === 'NamedType'; + +export const getScalarType = (config: FlutterFreezedPluginConfig, scalar: string): string => { + if (config?.customScalars?.[scalar]) { + return config.customScalars[scalar]; + } + if (DART_SCALARS[scalar]) { + return DART_SCALARS[scalar]; + } + return scalar; +}; + +//#endregion + +//#region NodeRepository classes +/** + * stores an instance of `ObjectTypeDefinitionNode` or `InputObjectTypeDefinitionNode` using the node name as the key + * and returns that node when replacing placeholders + * */ +export class NodeRepository { + private _store: Record = {}; + + get(key: string): ObjectType | undefined { + return this._store[key]; + } + + register(node: ObjectType): ObjectType { + this._store[node.name.value] = node; + return node; } } + +//#endregion diff --git a/packages/plugins/dart/flutter-freezed/tests/config.spec.ts b/packages/plugins/dart/flutter-freezed/tests/config.spec.ts index 5c36b475f..6881004fc 100644 --- a/packages/plugins/dart/flutter-freezed/tests/config.spec.ts +++ b/packages/plugins/dart/flutter-freezed/tests/config.spec.ts @@ -1,427 +1,98 @@ -import { plugin } from '@graphql-codegen/flutter-freezed'; -import { DefaultFreezedPluginConfig, getFreezedConfigValue } from '../src/utils'; -import { defaultConfig, typeConfig } from './config'; -import { - baseSchema, - cyclicSchema, - enumSchema, - extendedBaseSchema, - nonNullableListWithCustomScalars, - simpleUnionSchema, -} from './schema'; +import { DART_KEYWORDS, DART_SCALARS, FlutterFreezedPluginConfig } from '../src/config'; +import { defaultFreezedConfig, defaultFreezedPluginConfig } from '../src/utils'; +import { customDecoratorsConfig, typeConfig } from './config'; describe('flutter-freezed-plugin-config', () => { + it('should return the built-in Dart scalar types', () => { + expect(DART_SCALARS).toMatchObject({ + ID: 'String', + String: 'String', + Boolean: 'bool', + Int: 'int', + Float: 'double', + DateTime: 'DateTime', + }); + }); + + it('checks that all Dart language keywords are accounted for', () => { + expect(Object.keys(DART_KEYWORDS)).toHaveLength(78); + expect(Object.values(DART_KEYWORDS).filter((v: string) => v === 'built-in')).toHaveLength(23); + expect(Object.values(DART_KEYWORDS).filter((v: string) => v === 'context')).toHaveLength(5); + expect(Object.values(DART_KEYWORDS).filter((v: string) => v === 'async-reserved')).toHaveLength(2); + expect(Object.values(DART_KEYWORDS).filter((v: string) => v === 'reserved')).toHaveLength(48); + }); + it('should return the default plugin values', () => { - const config = defaultConfig; + const config: FlutterFreezedPluginConfig = defaultFreezedPluginConfig; expect(config.camelCasedEnums).toBe(true); expect(config.customScalars).toMatchObject({}); expect(config.fileName).toBe('app_models'); + expect(config.globalFreezedConfig).toMatchObject(defaultFreezedConfig); expect(config.ignoreTypes).toMatchObject([]); + expect(config.typeSpecificFreezedConfig).toMatchObject({}); }); it('should return the default globalFreezedConfig values', () => { - const config = defaultConfig; - expect(config.globalFreezedConfig.alwaysUseJsonKeyName).toBe(false); - expect(config.globalFreezedConfig.copyWith).toBeUndefined(); - expect(config.globalFreezedConfig.customDecorators).toMatchObject({}); - expect(config.globalFreezedConfig.defaultUnionConstructor).toBe(true); - expect(config.globalFreezedConfig.equal).toBeUndefined(); - expect(config.globalFreezedConfig.fromJsonToJson).toBe(true); - expect(config.globalFreezedConfig.immutable).toBe(true); - expect(config.globalFreezedConfig.makeCollectionsUnmodifiable).toBeUndefined(); - expect(config.globalFreezedConfig.mergeInputs).toMatchObject([]); - expect(config.globalFreezedConfig.mutableInputs).toBe(true); - expect(config.globalFreezedConfig.privateEmptyConstructor).toBe(true); - expect(config.globalFreezedConfig.unionKey).toBeUndefined(); - expect(config.globalFreezedConfig.unionValueCase).toBeUndefined(); + const config: FlutterFreezedPluginConfig = defaultFreezedPluginConfig; + const globalFreezedConfig = config?.globalFreezedConfig; + + expect(globalFreezedConfig?.alwaysUseJsonKeyName).toBe(false); + expect(globalFreezedConfig?.copyWith).toBeUndefined(); + expect(globalFreezedConfig?.customDecorators).toMatchObject({}); + expect(globalFreezedConfig?.dartKeywordEscapeCasing).toBeUndefined(); + expect(globalFreezedConfig?.dartKeywordEscapePrefix).toBeUndefined(); + expect(globalFreezedConfig?.dartKeywordEscapeSuffix).toBe('_'); + expect(globalFreezedConfig?.equal).toBeUndefined(); + expect(globalFreezedConfig?.escapeDartKeywords).toBe(true); + expect(globalFreezedConfig?.fromJsonToJson).toBe(true); + expect(globalFreezedConfig?.immutable).toBe(true); + expect(globalFreezedConfig?.makeCollectionsUnmodifiable).toBeUndefined(); + expect(globalFreezedConfig?.mergeInputs).toMatchObject([]); + expect(globalFreezedConfig?.mutableInputs).toBe(true); + expect(globalFreezedConfig?.privateEmptyConstructor).toBe(true); + expect(globalFreezedConfig?.unionKey).toBeUndefined(); + expect(globalFreezedConfig?.unionValueCase).toBeUndefined(); }); it('should return the typeSpecificFreezedConfig values', () => { const config = typeConfig; - const Starship = 'Starship'; - expect(config.typeSpecificFreezedConfig[Starship].config.alwaysUseJsonKeyName).toBe(true); - expect(config.typeSpecificFreezedConfig[Starship].config.copyWith).toBe(false); - expect(config.typeSpecificFreezedConfig[Starship].config.unionValueCase).toBe('FreezedUnionCase.pascal'); - }); - - describe('getFreezedConfigValue() returns the expect config value', () => { - const config = typeConfig; - const Starship = 'Starship'; - test('without a typeName should return the globalFreezedConfig value', () => { - expect(getFreezedConfigValue('alwaysUseJsonKeyName', config)).toBe(false); - expect(getFreezedConfigValue('copyWith', config)).toBeUndefined(); - expect(getFreezedConfigValue('unionValueCase', config)).toBe('FreezedUnionCase.camel'); - }); - - test('given a typeName should return the typeSpecificFreezedConfig value', () => { - expect(getFreezedConfigValue('alwaysUseJsonKeyName', config, Starship)).toBe(true); - expect(getFreezedConfigValue('copyWith', config, Starship)).toBe(false); - expect(getFreezedConfigValue('unionValueCase', config, Starship)).toBe('FreezedUnionCase.pascal'); - }); + const typeName = 'Starship'; + const typeSpecificFreezedConfig = config?.typeSpecificFreezedConfig?.[typeName]?.config; + + expect(config?.typeSpecificFreezedConfig?.[typeName]?.deprecated).toBe(true); + expect(typeSpecificFreezedConfig?.alwaysUseJsonKeyName).toBe(true); + expect(typeSpecificFreezedConfig?.copyWith).toBe(false); + expect(typeSpecificFreezedConfig?.customDecorators).toBeUndefined(); + expect(typeSpecificFreezedConfig?.dartKeywordEscapeCasing).toBeUndefined(); + expect(typeSpecificFreezedConfig?.dartKeywordEscapePrefix).toBeUndefined(); + expect(typeSpecificFreezedConfig?.dartKeywordEscapeSuffix).toBeUndefined(); + expect(typeSpecificFreezedConfig?.equal).toBeUndefined(); + expect(typeSpecificFreezedConfig?.escapeDartKeywords).toBeUndefined(); + expect(typeSpecificFreezedConfig?.fromJsonToJson).toBeUndefined(); + expect(typeSpecificFreezedConfig?.immutable).toBe(false); + expect(typeSpecificFreezedConfig?.makeCollectionsUnmodifiable).toBeUndefined(); + expect(typeSpecificFreezedConfig?.mergeInputs).toBeUndefined(); + expect(typeSpecificFreezedConfig?.mutableInputs).toBeUndefined(); + expect(typeSpecificFreezedConfig?.privateEmptyConstructor).toBeUndefined(); + expect(typeSpecificFreezedConfig?.unionKey).toBeUndefined(); + expect(typeSpecificFreezedConfig?.unionValueCase).toBe('FreezedUnionCase.pascal'); }); - describe('configuring the plugin', () => { - test('the imported packages', () => { - expect(plugin(baseSchema, [], new DefaultFreezedPluginConfig({ fileName: 'graphql_models' }))) - .toContain(`import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:flutter/foundation.dart'; - -part graphql_models.dart; -part 'graphql_models.g.dart'; -`); - - expect(plugin(baseSchema, [], new DefaultFreezedPluginConfig({ fileName: 'my_file_name.dart' }))) - .toContain(`import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:flutter/foundation.dart'; - -part my_file_name.dart; -part 'my_file_name.g.dart'; -`); - }); - - test('the casing of Enum fields ', () => { - expect(plugin(enumSchema, [], new DefaultFreezedPluginConfig({ camelCasedEnums: true }))).toContain(`enum Episode{ - @JsonKey(name: NEWHOPE) newhope - @JsonKey(name: EMPIRE) empire - @JsonKey(name: JEDI) jedi -}`); - - expect(plugin(enumSchema, [], new DefaultFreezedPluginConfig({ camelCasedEnums: false }))) - .toContain(`enum Episode{ - NEWHOPE - EMPIRE - JEDI -}`); - }); - - it('ignores these types ', () => { - expect(plugin(baseSchema, [], new DefaultFreezedPluginConfig({ ignoreTypes: ['PersonType'] }))).not.toContain( - `PersonType` - ); - }); - - it('should handle custom scalars', () => { - expect( - plugin( - nonNullableListWithCustomScalars, - [], - new DefaultFreezedPluginConfig({ - customScalars: { - jsonb: 'Map', - timestamptz: 'DateTime', - UUID: 'String', - }, - }) - ) - ).toBe(`import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:flutter/foundation.dart'; - -part app_models.dart; -part 'app_models.g.dart'; - -@freezed -class ComplexType with _$ComplexType { - const ComplexType._(); - - const factory ComplexType({ - List? a, - List? b, - required List c, - List?>? d, - List>? e, - required List> f, - Map? g, - required DateTime h, - required String i, - }) = _ComplexType; - - factory ComplexType.fromJson(Map json) => _ComplexTypeFromJson(json); -}`); - }); - - it('using @JsonKey(name: "fieldName") for fields that are not camelCased', () => { - expect( - plugin( - extendedBaseSchema, - [], - new DefaultFreezedPluginConfig({ - globalFreezedConfig: { - alwaysUseJsonKeyName: true, - }, - typeSpecificFreezedConfig: { - PersonType: { - config: { - alwaysUseJsonKeyName: false, - }, - }, - }, - }) - ) - ).toBe(`import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:flutter/foundation.dart'; - -part app_models.dart; -part 'app_models.g.dart'; - -@freezed -class BaseType with _$BaseType { - const BaseType._(); - - const factory BaseType({ - @JsonKey(name: 'id') - String? id, - @JsonKey(name: 'primaryKey') - required String primaryKey, - @JsonKey(name: 'CompositeForeignKey') - required String compositeForeignKey, - }) = _BaseType; - - factory BaseType.fromJson(Map json) => _BaseTypeFromJson(json); -} - -@freezed -class PersonType with _$PersonType { - const PersonType._(); - - const factory PersonType({ - String? id, - required String name, - required String primaryKey, - @JsonKey(name: 'CompositeForeignKey') - required String compositeForeignKey, - }) = _PersonType; - - factory PersonType.fromJson(Map json) => _PersonTypeFromJson(json); -}`); - }); - - describe('using defaultUnionConstructor & privateEmptyConstructor ', () => { - it('generates empty constructors for Union Types and mergedInputs and a private empty constructor to allow getter and methods to work on the class', () => {}); - // this is enabled by default - let result = plugin(simpleUnionSchema, [], new DefaultFreezedPluginConfig({})); - - // contains defaultUnionConstructor - expect(result).toContain('const factory AuthWithOtpInput() = _AuthWithOtpInput;'); - // contains privateEmptyConstructor - expect(result).toContain('const VerifyOtpInput._();'); - // expected output - expect(result).toBe(`import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:flutter/foundation.dart'; - -part app_models.dart; -part 'app_models.g.dart'; - -@unfreezed -class RequestOtpInput with _$RequestOtpInput { - const RequestOtpInput._(); - - const factory RequestOtpInput({ - String? email, - String? phoneNumber, - }) = _RequestOtpInput; - - factory RequestOtpInput.fromJson(Map json) => _RequestOtpInputFromJson(json); -} - -@unfreezed -class VerifyOtpInput with _$VerifyOtpInput { - const VerifyOtpInput._(); - - const factory VerifyOtpInput({ - String? email, - String? phoneNumber, - required String otpCode, - }) = _VerifyOtpInput; - - factory VerifyOtpInput.fromJson(Map json) => _VerifyOtpInputFromJson(json); -} - -@freezed -class AuthWithOtpInput with _$AuthWithOtpInput { - const AuthWithOtpInput._(); - - const factory AuthWithOtpInput() = _AuthWithOtpInput; - - const factory AuthWithOtpInput.requestOtpInput({ - String? email, - String? phoneNumber, - }) = RequestOtpInput; - - const factory AuthWithOtpInput.verifyOtpInput({ - String? email, - String? phoneNumber, - required String otpCode, - }) = VerifyOtpInput; - - factory AuthWithOtpInput.fromJson(Map json) => _AuthWithOtpInputFromJson(json); -}`); - - // disabling the default config - result = plugin( - simpleUnionSchema, - [], - new DefaultFreezedPluginConfig({ - globalFreezedConfig: { - defaultUnionConstructor: false, - }, - typeSpecificFreezedConfig: { - VerifyOTPInput: { - config: { - privateEmptyConstructor: false, - }, - }, - }, - }) - ); - - // does NOT contain defaultUnionConstructor - expect(result).not.toContain('const factory AuthWithOtpInput() = _AuthWithOtpInput;'); - // does NOT contain privateEmptyConstructor - expect(result).not.toContain('const VerifyOtpInput._();'); - // expected output - expect(result).toBe(`import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:flutter/foundation.dart'; - -part app_models.dart; -part 'app_models.g.dart'; - -@unfreezed -class RequestOtpInput with _$RequestOtpInput { - const RequestOtpInput._(); - - const factory RequestOtpInput({ - String? email, - String? phoneNumber, - }) = _RequestOtpInput; - - factory RequestOtpInput.fromJson(Map json) => _RequestOtpInputFromJson(json); -} - -@unfreezed -class VerifyOtpInput with _$VerifyOtpInput { - const factory VerifyOtpInput({ - String? email, - String? phoneNumber, - required String otpCode, - }) = _VerifyOtpInput; - - factory VerifyOtpInput.fromJson(Map json) => _VerifyOtpInputFromJson(json); -} - -@freezed -class AuthWithOtpInput with _$AuthWithOtpInput { - const AuthWithOtpInput._(); - - const factory AuthWithOtpInput.requestOtpInput({ - String? email, - String? phoneNumber, - }) = RequestOtpInput; - - const factory AuthWithOtpInput.verifyOtpInput({ - String? email, - String? phoneNumber, - required String otpCode, - }) = VerifyOtpInput; - - factory AuthWithOtpInput.fromJson(Map json) => _AuthWithOtpInputFromJson(json); -}`); - }); - - it('to be immutable OR immutable but configurable OR mutable ', () => { - expect( - plugin( - cyclicSchema, - [], - new DefaultFreezedPluginConfig({ - globalFreezedConfig: { - immutable: true, - mutableInputs: true, - }, - typeSpecificFreezedConfig: { - BaseAInput: { - config: { - immutable: true, - mutableInputs: false, // takes precedence - }, - }, - BaseBInput: { - config: { - immutable: false, - mutableInputs: false, // takes precedence - }, - }, - BaseCInput: { - config: { - immutable: false, - mutableInputs: true, // takes precedence - }, - }, - Base: { - config: { - copyWith: false, - fromJsonToJson: false, - makeCollectionsUnmodifiable: true, - unionValueCase: 'FreezedUnionCase.pascal', - }, - }, - }, - }) - ) - ).toBe(`import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:flutter/foundation.dart'; - -part app_models.dart; -part 'app_models.g.dart'; - -@freezed -class BaseAInput with _$BaseAInput { - const BaseAInput._(); - - const factory BaseAInput({ - required BaseBInput b, - }) = _BaseAInput; - - factory BaseAInput.fromJson(Map json) => _BaseAInputFromJson(json); -} - -@unfreezed -class BaseBInput with _$BaseBInput { - const BaseBInput._(); - - factory BaseBInput({ - required BaseCInput c, - }) = _BaseBInput; - - factory BaseBInput.fromJson(Map json) => _BaseBInputFromJson(json); -} - -@unfreezed -class BaseCInput with _$BaseCInput { - const BaseCInput._(); - - factory BaseCInput({ - required BaseAInput a, - }) = _BaseCInput; - - factory BaseCInput.fromJson(Map json) => _BaseCInputFromJson(json); -} - -@Freezed( - copyWith: false, - makeCollectionsUnmodifiable: true, - unionValueCase: 'FreezedUnionCase.pascal', -) -class Base with _$Base { - const Base._(); - - const factory Base({ - String? id, - }) = _Base; - -}`); - }); + it('should return the values of the field of the Droid Type', () => { + const config = customDecoratorsConfig; + const typeName = 'Droid'; + const fieldName = 'id'; + const decorator = '@NanoId'; + const fieldConfig = config?.typeSpecificFreezedConfig?.[typeName]?.fields?.[fieldName]; + + expect(fieldConfig?.final).toBeUndefined(); + expect(fieldConfig?.deprecated).toBeUndefined(); + expect(fieldConfig?.defaultValue).toBeUndefined(); + expect(fieldConfig?.customDecorators?.[decorator].applyOn).toMatchObject(['union_factory_parameter']); + expect(fieldConfig?.customDecorators?.[decorator].arguments).toMatchObject([ + 'size: 16', + 'alphabets: NanoId.ALPHA_NUMERIC', + ]); + expect(fieldConfig?.customDecorators?.[decorator].mapsToFreezedAs).toBe('custom'); }); }); -/* it('should ', () => {}); - it('should ', () => {}); - it('should ', () => {}) */ diff --git a/packages/plugins/dart/flutter-freezed/tests/config.ts b/packages/plugins/dart/flutter-freezed/tests/config.ts index 3c6d0ada4..d4cbae6ee 100644 --- a/packages/plugins/dart/flutter-freezed/tests/config.ts +++ b/packages/plugins/dart/flutter-freezed/tests/config.ts @@ -1,23 +1,24 @@ -import { DefaultFreezedPluginConfig } from '../src/utils'; +import { FlutterFreezedPluginConfig } from '../src/config'; +import { mergeConfig } from '../src/utils'; -export const defaultConfig = new DefaultFreezedPluginConfig(); - -export const typeConfig = new DefaultFreezedPluginConfig({ +export const typeConfig: FlutterFreezedPluginConfig = mergeConfig({ globalFreezedConfig: { unionValueCase: 'FreezedUnionCase.camel', }, typeSpecificFreezedConfig: { Starship: { + deprecated: true, config: { alwaysUseJsonKeyName: true, copyWith: false, + immutable: false, unionValueCase: 'FreezedUnionCase.pascal', }, }, }, }); -export const customDecoratorsConfig = new DefaultFreezedPluginConfig({ +export const customDecoratorsConfig: FlutterFreezedPluginConfig = mergeConfig({ globalFreezedConfig: { customDecorators: { '@JsonSerializable(explicitToJson: true)': { @@ -52,21 +53,14 @@ export const customDecoratorsConfig = new DefaultFreezedPluginConfig({ }, }); -export const fullDemoConfig = new DefaultFreezedPluginConfig({ - camelCasedEnums: true, - fileName: 'app_models', +export const fullDemoConfig: FlutterFreezedPluginConfig = mergeConfig({ customScalars: { jsonb: 'Map', timestamptz: 'DateTime', UUID: 'String', }, - ignoreTypes: [], globalFreezedConfig: { - immutable: true, - privateEmptyConstructor: true, mergeInputs: ['Create$Input', 'Upsert$Input', 'Delete$Input'], - defaultUnionConstructor: true, - mutableInputs: true, customDecorators: { '@JsonSerializable(explicitToJson: true)': { applyOn: ['class'], diff --git a/packages/plugins/dart/flutter-freezed/tests/plugin.spec.ts b/packages/plugins/dart/flutter-freezed/tests/plugin.spec.ts deleted file mode 100644 index 553d80fb9..000000000 --- a/packages/plugins/dart/flutter-freezed/tests/plugin.spec.ts +++ /dev/null @@ -1,284 +0,0 @@ -import { plugin } from '../src'; -import { fullDemoConfig } from './config'; -import { fullSchema } from './schema'; - -/** plugin test */ -describe('flutter-freezed: plugin config', () => { - test('full plugin test: expect generated code to be as configured', () => { - const result = plugin(fullSchema, [], fullDemoConfig); - - expect(result).toBe(`import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:flutter/foundation.dart'; - -part app_models.dart; -part 'app_models.g.dart'; - -enum Episode{ - @JsonKey(name: NEWHOPE) newhope - @JsonKey(name: EMPIRE) empire - @JsonKey(name: JEDI) jedi -} - -@freezed -@JsonSerializable(explicitToJson: true) -class Movie with _$Movie { - const Movie._(); - - const factory Movie({ - required String id, - required String title, - }) = _Movie; - - const factory Movie.createInput({ - required String title, - }) = CreateMovieInput; - - const factory Movie.upsertInput({ - required String id, - required String title, - }) = UpsertMovieInput; - - const factory Movie.deleteInput({ - required String id, - }) = DeleteMovieInput; - - factory Movie.fromJson(Map json) => _MovieFromJson(json); -} - -@unfreezed -@JsonSerializable(explicitToJson: true) -class CreateMovieInput with _$CreateMovieInput { - const CreateMovieInput._(); - - const factory CreateMovieInput.createInput({ - required String title, - }) = CreateMovieInput; - - factory CreateMovieInput.fromJson(Map json) => _CreateMovieInputFromJson(json); -} - -@unfreezed -@JsonSerializable(explicitToJson: true) -class UpsertMovieInput with _$UpsertMovieInput { - const UpsertMovieInput._(); - - const factory UpsertMovieInput.upsertInput({ - required String id, - required String title, - }) = UpsertMovieInput; - - factory UpsertMovieInput.fromJson(Map json) => _UpsertMovieInputFromJson(json); -} - -@unfreezed -@JsonSerializable(explicitToJson: true) -class UpdateMovieInput with _$UpdateMovieInput { - const UpdateMovieInput._(); - - const factory UpdateMovieInput({ - required String id, - String? title, - }) = _UpdateMovieInput; - - factory UpdateMovieInput.fromJson(Map json) => _UpdateMovieInputFromJson(json); -} - -@unfreezed -@JsonSerializable(explicitToJson: true) -class DeleteMovieInput with _$DeleteMovieInput { - const DeleteMovieInput._(); - - const factory DeleteMovieInput.deleteInput({ - required String id, - }) = DeleteMovieInput; - - factory DeleteMovieInput.fromJson(Map json) => _DeleteMovieInputFromJson(json); -} - -@Freezed( - copyWith: false, - equal: false, - unionValueCase: 'FreezedUnionCase.pascal', -) -@JsonSerializable(explicitToJson: true) -class Starship with _$Starship { - const factory Starship({ - @JsonKey(name: 'id') - required String id, - @JsonKey(name: 'name') - required String name, - @JsonKey(name: 'length') - double? length, - }) = _Starship; - - factory Starship.fromJson(Map json) => _StarshipFromJson(json); -} - -@freezed -@JsonSerializable(explicitToJson: true) -class MovieCharacter with _$MovieCharacter { - const MovieCharacter._(); - - const factory MovieCharacter({ - required String name, - required List appearsIn, - }) = _MovieCharacter; - - factory MovieCharacter.fromJson(Map json) => _MovieCharacterFromJson(json); -} - -@freezed -@JsonSerializable(explicitToJson: true) -class Human with _$Human { - const Human._(); - - const factory Human({ - required String id, - required String name, - List? friends, - required List appearsIn, - List? starships, - int? totalCredits, - }) = _Human; - - factory Human.fromJson(Map json) => _HumanFromJson(json); -} - -@unfreezed -@JsonSerializable(explicitToJson: true) -class Droid with _$Droid { - const Droid._(); - - factory Droid({ - required String id, - required String name, - List? friends, - required List appearsIn, - String? primaryFunction, - }) = _Droid; - -} - -@freezed -@JsonSerializable(explicitToJson: true) -class SearchResult with _$SearchResult { - const SearchResult._(); - - const factory SearchResult() = _SearchResult; - - const factory SearchResult.human({ - required String id, - required String name, - List? friends, - required List appearsIn, - List? starships, - int? totalCredits, - }) = Human; - - @FreezedUnionValue('BestDroid') - factory SearchResult.droid({ - @NanoId(size: 16, alphabets: NanoId.ALPHA_NUMERIC) - required String id, - required String name, - List? friends, - required List appearsIn, - String? primaryFunction, - }) = Droid; - - const factory SearchResult.starship({ - @JsonKey(name: 'id') - required String id, - @JsonKey(name: 'name') - required String name, - @JsonKey(name: 'length') - double? length, - }) = Starship; - - factory SearchResult.fromJson(Map json) => _SearchResultFromJson(json); -} - -@freezed -@JsonSerializable(explicitToJson: true) -class ComplexType with _$ComplexType { - const ComplexType._(); - - const factory ComplexType({ - List? a, - List? b, - required List c, - List?>? d, - List>? e, - required List> f, - Map? g, - required DateTime h, - required String i, - }) = _ComplexType; - - factory ComplexType.fromJson(Map json) => _ComplexTypeFromJson(json); -} - -@unfreezed -@JsonSerializable(explicitToJson: true) -class BaseAInput with _$BaseAInput { - const BaseAInput._(); - - const factory BaseAInput({ - required BaseBInput b, - }) = _BaseAInput; - - factory BaseAInput.fromJson(Map json) => _BaseAInputFromJson(json); -} - -@unfreezed -@JsonSerializable(explicitToJson: true) -class BaseBInput with _$BaseBInput { - const BaseBInput._(); - - const factory BaseBInput({ - required BaseCInput c, - }) = _BaseBInput; - - factory BaseBInput.fromJson(Map json) => _BaseBInputFromJson(json); -} - -@unfreezed -@JsonSerializable(explicitToJson: true) -class BaseCInput with _$BaseCInput { - const BaseCInput._(); - - const factory BaseCInput({ - required BaseAInput a, - }) = _BaseCInput; - - factory BaseCInput.fromJson(Map json) => _BaseCInputFromJson(json); -} - -@freezed -@JsonSerializable(explicitToJson: true) -class Base with _$Base { - const Base._(); - - const factory Base({ - String? id, - }) = _Base; - - const factory Base.aInput({ - required BaseBInput b, - }) = BaseAInput; - - const factory Base.bInput({ - required BaseCInput c, - }) = BaseBInput; - - const factory Base.cInput({ - required BaseAInput a, - }) = BaseCInput; - - const factory Base.createMovieInput({ - required String title, - }) = CreateMovieInput; - - factory Base.fromJson(Map json) => _BaseFromJson(json); -}`); - }); -}); diff --git a/packages/plugins/dart/flutter-freezed/tests/plugin.ts b/packages/plugins/dart/flutter-freezed/tests/plugin.ts deleted file mode 100644 index 3b79d9714..000000000 --- a/packages/plugins/dart/flutter-freezed/tests/plugin.ts +++ /dev/null @@ -1,211 +0,0 @@ -// import { buildSchema } from 'graphql'; -// import { plugin } from '../src'; -// import { DefaultFreezedPluginConfig, getFreezedConfigValue } from '../src/utils'; - -// describe('flutter-freezed', () => { -// const schema = buildSchema(/* GraphQL */ ` -// type Movie { -// id: ID! -// title: String! -// } - -// input CreateMovieInput { -// title: String! -// } - -// input UpsertMovieInput { -// id: ID! -// title: String! -// } - -// input UpdateMovieInput { -// id: ID! -// title: String -// } - -// input DeleteMovieInput { -// id: ID! -// } - -// enum Episode { -// NEWHOPE -// EMPIRE -// JEDI -// } - -// type Starship { -// id: ID! -// name: String! -// length: Float -// } - -// interface Character { -// id: ID! -// name: String! -// friends: [Character] -// appearsIn: [Episode]! -// } - -// type MovieCharacter { -// name: String! -// appearsIn: [Episode]! -// } - -// type Human implements Character { -// id: ID! -// name: String! -// friends: [MovieCharacter] -// appearsIn: [Episode]! -// starships: [Starship] -// totalCredits: Int -// } - -// type Droid implements Character { -// id: ID! -// name: String! -// friends: [MovieCharacter] -// appearsIn: [Episode]! -// primaryFunction: String -// } - -// union SearchResult = Human | Droid | Starship - -// # tests - -// scalar UUID -// scalar timestamptz -// scalar jsonb - -// # cyclic references/nested types -// input AInput { -// b: BInput! -// } - -// input BInput { -// c: CInput! -// } - -// input CInput { -// a: AInput! -// } - -// type ComplexType { -// a: [String] -// b: [ID!] -// c: [Boolean!]! -// d: [[Int]] -// e: [[Float]!] -// f: [[String]!]! -// g: jsonb -// h: timestamptz! -// i: UUID! -// } -// `); - -// const unionSchema = buildSchema(/* GraphQL */ ` -// enum Episode { -// NEWHOPE -// EMPIRE -// JEDI -// } - -// type Starship { -// id: ID! -// name: String! -// length: Float -// } - -// interface Character { -// id: ID! -// name: String! -// friends: [Character] -// appearsIn: [Episode]! -// } - -// type MovieCharacter { -// name: String! -// appearsIn: [Episode]! -// } - -// type Human implements Character { -// id: ID! -// name: String! -// friends: [MovieCharacter] -// appearsIn: [Episode]! -// starships: [Starship] -// totalCredits: Int -// } - -// type Droid implements Character { -// id: ID! -// name: String! -// friends: [MovieCharacter] -// appearsIn: [Episode]! -// primaryFunction: String -// } - -// union SearchResult = Human | Droid | Starship -// `); - -// it('Should greet', async () => { -// const result = await plugin(unionSchema, [], {}); - -// expect(result).toBe('Hi'); -// }); -// }); - -// describe('get freezed config value', () => { -// const config = new DefaultFreezedPluginConfig({ -// typeSpecificFreezedConfig: { -// Starship: { -// config: { -// immutable: false, -// }, -// }, -// }, -// }); - -// it('should return the typeSpecific config value', () => { -// expect(getFreezedConfigValue('immutable', config, 'Starship')).toBe(false); -// }); - -// /* it('should return the default value', () => { -// expect(getConfigValue(config, 'directiveMap')).toBe([]); -// }); - -// it('should return the default value', () => { -// expect(getConfigValue(config, 'fileName')).toBe('app_models'); -// }); - -// it('should return the default value', () => { -// expect(getConfigValue(config, 'globalFreezedConfig')).toBeInstanceOf(DefaultFreezedConfig); -// }); - -// it('should return the default value', () => { -// expect(getConfigValue(config, 'typeSpecificFreezedConfig')).toBe({}); -// }); - -// it('should return the default value', () => { -// expect(getConfigValue(config, 'ignoreTypes')).toBe([]); -// }); - -// it('should return the default value', () => { -// expect(getConfigValue(config, 'interfaceNamePrefix')).toBe(''); -// }); - -// it('should return the default value', () => { -// expect(getConfigValue(config, 'interfaceNameSuffix')).toBe('Interface'); -// }); - -// it('should return the default value', () => { -// expect(getConfigValue(config, 'lowercaseEnums')).toBeTruthy(); -// }); - -// it('should return the default value', () => { -// expect(getConfigValue(config, 'modular')).toBeTruthy(); -// }); */ - -// it('should return the default value', () => { -// expect(getFreezedConfigValue('immutable', config, 'Spaceship')).toBe(true); -// }); -// }); diff --git a/packages/plugins/dart/flutter-freezed/tests/schema.ts b/packages/plugins/dart/flutter-freezed/tests/schema.ts index 4970ac207..e2f68f37f 100644 --- a/packages/plugins/dart/flutter-freezed/tests/schema.ts +++ b/packages/plugins/dart/flutter-freezed/tests/schema.ts @@ -5,14 +5,20 @@ export const enumSchema = buildSchema(/* GraphQL */ ` NEWHOPE EMPIRE JEDI + VOID + void + IN + in + ELSE + else + SWITCH + switch + FACTORY + factory } `); export const baseSchema = buildSchema(/* GraphQL */ ` - type BaseType { - id: String - } - type PersonType { id: String name: String! @@ -20,17 +26,40 @@ export const baseSchema = buildSchema(/* GraphQL */ ` `); export const extendedBaseSchema = buildSchema(/* GraphQL */ ` - type BaseType { - id: String - primaryKey: String! - CompositeForeignKey: String! + type PersonType { + id: String! + name: String! + status: String } - type PersonType { - id: String + input CreatePersonInput { name: String! - primaryKey: String! - CompositeForeignKey: String! + status: String + } + + input UpdatePersonInput { + name: String + status: String + } + + input DeletePersonInput { + id: String! + } + + enum Episode { + NEWHOPE + EMPIRE + JEDI + VOID + void + IN + in + ELSE + else + SWITCH + switch + FACTORY + factory } `); @@ -64,6 +93,42 @@ export const starWarsSchema = buildSchema(/* GraphQL */ ` NEWHOPE EMPIRE JEDI + VOID + void + IN + in + ELSE + else + SWITCH + switch + FACTORY + factory + male + female + phoneNumber + } + + type Movie { + id: ID! + title: String! + } + + input CreateMovieInput { + title: String! + } + + input UpsertMovieInput { + id: ID! + title: String! + } + + input UpdateMovieInput { + id: ID! + title: String + } + + input DeleteMovieInput { + id: ID! } type Starship { @@ -154,121 +219,3 @@ export const nonNullableListWithCustomScalars = buildSchema(/* GraphQL */ ` i: UUID! } `); - -export const fullSchema = buildSchema(/* GraphQL */ ` - # ******************************************* - # custom scalars * - # ******************************************* - scalar UUID - scalar timestamptz - scalar jsonb - - # ******************************************* - # enums * - # ******************************************* - enum Episode { - NEWHOPE - EMPIRE - JEDI - } - - # ******************************************* - # object type with input types * - # ******************************************* - type Movie { - id: ID! - title: String! - } - - input CreateMovieInput { - title: String! - } - - input UpsertMovieInput { - id: ID! - title: String! - } - - input UpdateMovieInput { - id: ID! - title: String - } - - input DeleteMovieInput { - id: ID! - } - - # ******************************************* - # union type * - # ******************************************* - type Starship { - id: ID! - name: String! - length: Float - } - - interface Character { - id: ID! - name: String! - friends: [Character] - appearsIn: [Episode]! - } - - type MovieCharacter { - name: String! - appearsIn: [Episode]! - } - - type Human implements Character { - id: ID! - name: String! - friends: [MovieCharacter] - appearsIn: [Episode]! - starships: [Starship] - totalCredits: Int - } - - type Droid implements Character { - id: ID! - name: String! - friends: [MovieCharacter] - appearsIn: [Episode]! - primaryFunction: String - } - - union SearchResult = Human | Droid | Starship - - # ******************************************* - # (non)-nullables * - # ******************************************* - type ComplexType { - a: [String] - b: [ID!] - c: [Boolean!]! - d: [[Int]] - e: [[Float]!] - f: [[String]!]! - g: jsonb - h: timestamptz! - i: UUID! - } - - # ******************************************* - # cyclic inputs * - # ******************************************* - input BaseAInput { - b: BaseBInput! - } - - input BaseBInput { - c: BaseCInput! - } - - input BaseCInput { - a: BaseAInput! - } - - type Base { - id: String - } -`); diff --git a/packages/plugins/dart/flutter-freezed/tests/utils.spec.ts b/packages/plugins/dart/flutter-freezed/tests/utils.spec.ts index 7ef057e1d..76681bd66 100644 --- a/packages/plugins/dart/flutter-freezed/tests/utils.spec.ts +++ b/packages/plugins/dart/flutter-freezed/tests/utils.spec.ts @@ -1,84 +1,553 @@ -import { CustomDecorator } from '../src/config'; -import { getCustomDecorators, getFreezedConfigValue, transformCustomDecorators } from '../src/utils'; -import { customDecoratorsConfig, defaultConfig, typeConfig } from './config'; - -/** utils test */ -describe('flutter-freezed: utils & helpers', () => { - test('getFreezedConfigValue(): returns the expected value for globalFreezedConfig and typeSpecificFreezedConfig', () => { - const Starship = 'Starship'; - - expect(getFreezedConfigValue('alwaysUseJsonKeyName', defaultConfig)).toBe(false); - expect(getFreezedConfigValue('alwaysUseJsonKeyName', typeConfig, Starship)).toBe(true); - - expect(getFreezedConfigValue('copyWith', defaultConfig)).toBe(undefined); - expect(getFreezedConfigValue('copyWith', typeConfig, Starship)).toBe(false); - - expect(getFreezedConfigValue('customDecorators', defaultConfig)).toMatchObject({}); - expect(getFreezedConfigValue('defaultUnionConstructor', defaultConfig)).toBe(true); - expect(getFreezedConfigValue('equal', defaultConfig)).toBe(undefined); - expect(getFreezedConfigValue('fromJsonToJson', defaultConfig)).toBe(true); - expect(getFreezedConfigValue('immutable', defaultConfig)).toBe(true); - expect(getFreezedConfigValue('makeCollectionsUnmodifiable', defaultConfig)).toBe(undefined); - expect(getFreezedConfigValue('mergeInputs', defaultConfig)).toMatchObject([]); - expect(getFreezedConfigValue('mutableInputs', defaultConfig)).toBe(true); - expect(getFreezedConfigValue('privateEmptyConstructor', defaultConfig)).toBe(true); - expect(getFreezedConfigValue('unionKey', defaultConfig)).toBe(undefined); - - expect(getFreezedConfigValue('unionValueCase', defaultConfig)).toBe(undefined); - expect(getFreezedConfigValue('unionValueCase', typeConfig, Starship)).toBe('FreezedUnionCase.pascal'); - }); +import { transformSchemaAST } from '@graphql-codegen/schema-ast'; +import { indent } from '@graphql-codegen/visitor-plugin-common'; +import { DartIdentifierCasing, FlutterFreezedPluginConfig } from '../src/config'; +import { FreezedFactoryBlock } from '../src/freezed-declaration-blocks'; +import { + buildBlock, + buildBlockName, + buildClassFooter, + buildClassHeader, + buildEnumFooter, + buildEnumHeader, + buildImportStatements, + dartCasing, + defaultFreezedConfig, + defaultFreezedPluginConfig, + escapeDartKeyword, + getFreezedConfigValue, + mergeConfig, + nodeIsObjectType, + NodeRepository, + NodeType, +} from '../src/utils'; +import { customDecoratorsConfig, fullDemoConfig, typeConfig } from './config'; +import { starWarsSchema } from './schema'; - describe('customDecorators', () => { - const globalCustomDecorators = getCustomDecorators(customDecoratorsConfig, ['class']); +const { + ast: { definitions: astNodesList }, +} = transformSchemaAST(starWarsSchema, fullDemoConfig); - const droidCustomDecorators = getCustomDecorators(customDecoratorsConfig, ['class', 'union_factory'], 'Droid'); +const prefixConfig = mergeConfig({ + globalFreezedConfig: { dartKeywordEscapePrefix: 'k_', dartKeywordEscapeSuffix: undefined }, +}); - const idCustomDecorators = getCustomDecorators(customDecoratorsConfig, ['union_factory_parameter'], 'Droid', 'id'); +const suffixConfig = mergeConfig({ + globalFreezedConfig: { dartKeywordEscapePrefix: undefined, dartKeywordEscapeSuffix: '_k' }, +}); - test('getCustomDecorators()', () => { - expect(globalCustomDecorators).toMatchObject({ - '@JsonSerializable(explicitToJson: true)': { - applyOn: ['class'], - mapsToFreezedAs: 'custom', - }, +const prefixSuffixConfig = mergeConfig({ + globalFreezedConfig: { dartKeywordEscapePrefix: 'k_', dartKeywordEscapeSuffix: '_k' }, +}); + +describe('flutter-freezed-plugin-utils', () => { + describe('default values for plugin config', () => { + test('property: defaultFreezedConfig ==> has the default values', () => { + expect(defaultFreezedConfig).toMatchObject({ + alwaysUseJsonKeyName: false, + copyWith: undefined, + customDecorators: {}, + dartKeywordEscapeCasing: undefined, + dartKeywordEscapePrefix: undefined, + dartKeywordEscapeSuffix: '_', + escapeDartKeywords: true, + equal: undefined, + fromJsonToJson: true, + immutable: true, + makeCollectionsUnmodifiable: undefined, + mergeInputs: [], + mutableInputs: true, + privateEmptyConstructor: true, + unionKey: undefined, + unionValueCase: undefined, }); + }); - expect(droidCustomDecorators).toMatchObject({ - '@JsonSerializable(explicitToJson: true)': { - applyOn: ['class'], - mapsToFreezedAs: 'custom', - }, - '@FreezedUnionValue': { - applyOn: ['union_factory'], - arguments: ["'BestDroid'"], - mapsToFreezedAs: 'custom', - }, + test('property: defaultFreezedPluginConfig ==> has the default values', () => { + expect(defaultFreezedPluginConfig).toMatchObject({ + camelCasedEnums: true, + customScalars: {}, + fileName: 'app_models', + globalFreezedConfig: { ...defaultFreezedConfig }, + typeSpecificFreezedConfig: {}, + ignoreTypes: [], }); + }); - expect(idCustomDecorators).toMatchObject({ - '@NanoId': { - applyOn: ['union_factory_parameter'], - arguments: ['size: 16', 'alphabets: NanoId.ALPHA_NUMERIC'], - mapsToFreezedAs: 'custom', + test('method: mergeConfig() ==> extends the default config', () => { + expect(mergeConfig()).toMatchObject(defaultFreezedPluginConfig); + expect(mergeConfig().globalFreezedConfig).toMatchObject(defaultFreezedConfig); + expect(mergeConfig().typeSpecificFreezedConfig).toMatchObject({}); + expect(mergeConfig(typeConfig)).toMatchObject(typeConfig); + expect(mergeConfig().fileName).toBe(fullDemoConfig.fileName); + + const expected: FlutterFreezedPluginConfig = { + camelCasedEnums: true, + customScalars: {}, + fileName: 'app_models', + globalFreezedConfig: { + ...defaultFreezedConfig, + customDecorators: { + '@JsonSerializable(explicitToJson: true)': { + applyOn: ['class'], + mapsToFreezedAs: 'custom', + }, + }, }, - }); + ignoreTypes: [], + typeSpecificFreezedConfig: { + Droid: { + config: { + customDecorators: { + '@FreezedUnionValue': { + applyOn: ['union_factory'], + arguments: ["'BestDroid'"], + mapsToFreezedAs: 'custom', + }, + }, + }, + fields: { + id: { + customDecorators: { + '@NanoId': { + applyOn: ['union_factory_parameter'], + arguments: ['size: 16', 'alphabets: NanoId.ALPHA_NUMERIC'], + mapsToFreezedAs: 'custom', + }, + }, + }, + }, + }, + }, + }; + + expect(customDecoratorsConfig).toMatchObject(expected); + }); + }); + + test('method: nodeIsObjectType() => returns true if node is an ObjectType', () => { + const expected = [false, true, true, true, true, true, true, false, true, true, true, false]; + expect(astNodesList.map(nodeIsObjectType)).toEqual(expected); + }); + + describe('method: getFreezedConfigValue() ==> ', () => { + const config = typeConfig; + const typeName = 'Starship'; + + test('without a typeName, returns the value from the globalFreezedConfig(target)', () => { + expect(getFreezedConfigValue('alwaysUseJsonKeyName', config)).toBe(false); + expect(getFreezedConfigValue('copyWith', config)).toBeUndefined(); + expect(getFreezedConfigValue('customDecorators', config)).toMatchObject({}); + expect(getFreezedConfigValue('dartKeywordEscapeCasing', config)).toBeUndefined(); + expect(getFreezedConfigValue('dartKeywordEscapePrefix', config)).toBeUndefined(); + expect(getFreezedConfigValue('dartKeywordEscapeSuffix', config)).toBe('_'); + expect(getFreezedConfigValue('equal', config)).toBeUndefined(); + expect(getFreezedConfigValue('escapeDartKeywords', config)).toBe(true); + expect(getFreezedConfigValue('fromJsonToJson', config)).toBe(true); + expect(getFreezedConfigValue('immutable', config)).toBe(true); + expect(getFreezedConfigValue('makeCollectionsUnmodifiable', config)).toBeUndefined(); + expect(getFreezedConfigValue('mergeInputs', config)).toMatchObject([]); + expect(getFreezedConfigValue('mutableInputs', config)).toBe(true); + expect(getFreezedConfigValue('privateEmptyConstructor', config)).toBe(true); + expect(getFreezedConfigValue('unionKey', config)).toBeUndefined(); + expect(getFreezedConfigValue('unionValueCase', config)).toBe('FreezedUnionCase.camel'); + }); + + test('without a typeName, will use the defaultValue if the globalFreezedConfig(target) values is undefined', () => { + expect(getFreezedConfigValue('copyWith', config, undefined, true)).toBe(true); + expect(getFreezedConfigValue('copyWith', config, undefined, false)).toBe(false); + + expect(getFreezedConfigValue('dartKeywordEscapeCasing', config, undefined, 'snake_case')).toBe('snake_case'); + expect(getFreezedConfigValue('dartKeywordEscapeCasing', config, undefined, 'camelCase')).toBe('camelCase'); + expect(getFreezedConfigValue('dartKeywordEscapeCasing', config, undefined, 'PascalCase')).toBe('PascalCase'); + + expect(getFreezedConfigValue('dartKeywordEscapePrefix', config, undefined, 'GQL_')).toBe('GQL_'); + expect(getFreezedConfigValue('dartKeywordEscapePrefix', config, undefined, 'ff')).toBe('ff'); + + expect(getFreezedConfigValue('equal', config, undefined, true)).toBe(true); + expect(getFreezedConfigValue('equal', config, undefined, false)).toBe(false); + + expect(getFreezedConfigValue('makeCollectionsUnmodifiable', config, undefined, true)).toBe(true); + expect(getFreezedConfigValue('makeCollectionsUnmodifiable', config, undefined, false)).toBe(false); + + expect(getFreezedConfigValue('unionKey', config, undefined, 'runtimeType')).toBe('runtimeType'); + expect(getFreezedConfigValue('unionKey', config, undefined, 'type')).toBe('type'); + }); + + test('given a typeName, returns the value from the typeSpecificFreezedConfig(target)', () => { + expect(getFreezedConfigValue('alwaysUseJsonKeyName', config, typeName)).toBe(true); + expect(getFreezedConfigValue('copyWith', config, typeName)).toBe(false); + expect(getFreezedConfigValue('immutable', config, typeName)).toBe(false); + expect(getFreezedConfigValue('unionValueCase', config, typeName)).toBe('FreezedUnionCase.pascal'); + }); + + test('given a typeName, falls back to the globalFreezedConfig if they value is undefined', () => { + expect(getFreezedConfigValue('customDecorators', config, typeName)).toMatchObject({}); + expect(getFreezedConfigValue('dartKeywordEscapeCasing', config, typeName)).toBeUndefined(); + expect(getFreezedConfigValue('dartKeywordEscapePrefix', config, typeName)).toBeUndefined(); + expect(getFreezedConfigValue('dartKeywordEscapeSuffix', config, typeName)).toBe('_'); + expect(getFreezedConfigValue('equal', config, typeName)).toBeUndefined(); + expect(getFreezedConfigValue('escapeDartKeywords', config, typeName)).toBe(true); + expect(getFreezedConfigValue('fromJsonToJson', config, typeName)).toBe(true); + expect(getFreezedConfigValue('makeCollectionsUnmodifiable', config, typeName)).toBeUndefined(); + expect(getFreezedConfigValue('mergeInputs', config, typeName)).toMatchObject([]); + expect(getFreezedConfigValue('mutableInputs', config, typeName)).toBe(true); + expect(getFreezedConfigValue('privateEmptyConstructor', config, typeName)).toBe(true); + expect(getFreezedConfigValue('unionKey', config, typeName)).toBeUndefined(); + }); + + test('given a typeName, will use the defaultValue if both typeSpecificFreezedConfig(target) and globalFreezedConfig(fallback) values is undefined', () => { + expect(getFreezedConfigValue('dartKeywordEscapeCasing', config, typeName, 'snake_case')).toBe('snake_case'); + expect(getFreezedConfigValue('dartKeywordEscapeCasing', config, typeName, 'camelCase')).toBe('camelCase'); + expect(getFreezedConfigValue('dartKeywordEscapeCasing', config, typeName, 'PascalCase')).toBe('PascalCase'); + + expect(getFreezedConfigValue('dartKeywordEscapePrefix', config, typeName, 'GQL_')).toBe('GQL_'); + expect(getFreezedConfigValue('dartKeywordEscapePrefix', config, typeName, 'ff')).toBe('ff'); + + expect(getFreezedConfigValue('equal', config, typeName, true)).toBe(true); + expect(getFreezedConfigValue('equal', config, typeName, false)).toBe(false); + + expect(getFreezedConfigValue('makeCollectionsUnmodifiable', config, typeName, true)).toBe(true); + expect(getFreezedConfigValue('makeCollectionsUnmodifiable', config, typeName, false)).toBe(false); + + expect(getFreezedConfigValue('unionKey', config, typeName, 'runtimeType')).toBe('runtimeType'); + expect(getFreezedConfigValue('unionKey', config, typeName, 'type')).toBe('type'); + }); + }); + + test('method: buildImportStatements() => returns a string of import statements', () => { + expect(buildImportStatements('SomeFileName')).toContain('some_file_name'); + expect(buildImportStatements('_Some-File_Name')).toContain('some_file_name'); + expect(buildImportStatements('Some file name')).toContain('some_file_name'); + expect(buildImportStatements('some-file-name.dart')).toContain('some_file_name.freezed.dart'); + expect(() => buildImportStatements('')).toThrow('fileName is required and must not be empty'); + + expect(buildImportStatements('/lib/models/some-file_name.dart')).toBe( + [ + `import 'package:freezed_annotation/freezed_annotation.dart';\n`, + `import 'package:flutter/foundation.dart';\n\n`, + `part 'some_file_name.freezed.dart';\n`, + `part 'some_file_name.g.dart';\n\n`, + ].join('') + ); + }); + + test('method: dartCasing() => ', () => { + expect(dartCasing('snake---- Case___ ME', 'snake_case')).toBe('snake_case_me'); + expect(dartCasing('Camel_ case- -- - ME', 'camelCase')).toBe('camelCaseMe'); + expect(dartCasing('pascal-- --case _ ME', 'PascalCase')).toBe('PascalCaseMe'); + expect(dartCasing('lE-AvE mE A-l_o_n-e')).toBe('lE-AvE mE A-l_o_n-e'); + }); + + describe('methods: escapeDartKey() and buildBlockName() => ', () => { + const config = mergeConfig(); + + type T = { + title: string; + args: { + config: FlutterFreezedPluginConfig; + blockName: string; + typeName?: string; + expected: string; + casing?: DartIdentifierCasing; + decorateWithAtJsonKey?: boolean; + }[]; + }[]; + + const data: T = [ + { + title: 'defaultConfig => NOT a valid Dart Language Keywords => it should NOT escape it', + args: ['NEWHOPE', 'EMPIRE', 'JEDI', 'VOID', 'IN', 'IF', 'ELSE', 'SWITCH', 'FACTORY'].map(v => { + return { config, blockName: v, expected: v }; + }), + }, + { + title: 'defaultConfig => valid Dart Language Keywords => it should escape it', + args: ['void', 'in', 'if', 'else', 'switch', 'factory'].map(v => { + return { config, blockName: v, expected: `${v}_` }; + }), + }, + { + title: + 'defaultConfig => withCasing => should ignore both dartKeywordEscapeCasing and buildBlockName casing if casedBlockName is still a valid Dart Language Keyword', + args: [undefined, 'snake_case', 'camelCase'].map(casing => { + return { config, blockName: 'void', casing, expected: 'void_' }; + }), + }, + { + title: + 'defaultConfig => decorateWithAtJsonKey => should ignore both dartKeywordEscapeCasing and buildBlockName casing if casedBlockName is still a valid Dart Language Keyword', + args: [undefined, 'snake_case', 'camelCase'].map(casing => { + return { + config, + blockName: 'void', + casing, + decorateWithAtJsonKey: true, + expected: `@JsonKey(name: 'void') void_`, + }; + }), + }, + { + title: + 'prefixConfig => withCasing => should ignore both dartKeywordEscapeCasing and buildBlockName casing if casedBlockName is still a valid Dart Language Keyword', + args: [undefined, 'snake_case', 'camelCase'].map(casing => { + return { + config: prefixConfig, + blockName: 'void', + casing, + expected: 'k_void', + }; + }), + }, + { + title: + 'prefixConfig => decorateWithAtJsonKey => should ignore both dartKeywordEscapeCasing and buildBlockName casing if casedBlockName is still a valid Dart Language Keyword', + args: [undefined, 'snake_case', 'camelCase'].map(casing => { + return { + config: prefixConfig, + blockName: 'void', + casing, + decorateWithAtJsonKey: true, + expected: `@JsonKey(name: 'void') k_void`, + }; + }), + }, + { + title: + 'suffixConfig => withCasing => should ignore both dartKeywordEscapeCasing and buildBlockName casing if casedBlockName is still a valid Dart Language Keyword', + args: [undefined, 'snake_case', 'camelCase'].map(casing => { + return { + config: suffixConfig, + blockName: 'void', + casing, + expected: 'void_k', + }; + }), + }, + { + title: + 'suffixConfig => decorateWithAtJsonKey => should ignore both dartKeywordEscapeCasing and buildBlockName casing if casedBlockName is still a valid Dart Language Keyword', + args: [undefined, 'snake_case', 'camelCase'].map(casing => { + return { + config: suffixConfig, + blockName: 'void', + casing, + decorateWithAtJsonKey: true, + expected: `@JsonKey(name: 'void') void_k`, + }; + }), + }, + { + title: + 'prefixSuffixConfig => withCasing => should ignore both dartKeywordEscapeCasing and buildBlockName casing if casedBlockName is still a valid Dart Language Keyword', + args: [undefined, 'snake_case', 'camelCase'].map(casing => { + return { + config: prefixSuffixConfig, + blockName: 'void', + casing, + expected: 'k_void_k', + }; + }), + }, + { + title: + 'prefixSuffixConfig => decorateWithAtJsonKey => should ignore both dartKeywordEscapeCasing and buildBlockName casing if casedBlockName is still a valid Dart Language Keyword', + args: [undefined, 'snake_case', 'camelCase'].map(casing => { + return { + config: prefixSuffixConfig, + blockName: 'void', + casing, + decorateWithAtJsonKey: true, + expected: `@JsonKey(name: 'void') k_void_k`, + }; + }), + }, + ]; + + test.each(data[0].args)(data[0].title, ({ config, blockName, expected }) => { + expect(escapeDartKeyword(config, blockName)).toBe(expected); + expect(buildBlockName(config, blockName)).toBe(expected); + }); + + test.each(data[1].args)(data[1].title, ({ config, blockName, expected }) => { + expect(escapeDartKeyword(config, blockName)).toBe(expected); + expect(buildBlockName(config, blockName)).toBe(expected); + }); + + test.each(data[2].args)(data[2].title, ({ config, blockName, casing, expected }) => { + expect(escapeDartKeyword(config, blockName)).toBe(expected); + expect(buildBlockName(config, blockName, undefined, casing)).toBe(expected); + expect(buildBlockName(config, blockName, undefined, 'PascalCase')).toBe('Void'); + }); + + test.each(data[3].args)(data[3].title, ({ config, blockName, casing, decorateWithAtJsonKey, expected }) => { + expect(escapeDartKeyword(config, blockName)).toBe('void_'); + expect(buildBlockName(config, blockName, undefined, casing, decorateWithAtJsonKey)).toBe(expected); + expect(buildBlockName(config, blockName, undefined, 'PascalCase', decorateWithAtJsonKey)).toBe( + `@JsonKey(name: 'void') Void` + ); + }); + + test.each(data[4].args)(data[4].title, ({ config, blockName, casing, expected }) => { + expect(escapeDartKeyword(config, blockName)).toBe(expected); + expect(buildBlockName(config, blockName, undefined, casing)).toBe(casing === 'camelCase' ? 'kVoid' : expected); + expect(buildBlockName(config, blockName, undefined, 'PascalCase')).toBe('KVoid'); + }); + + test.each(data[5].args)(data[5].title, ({ config, blockName, casing, decorateWithAtJsonKey, expected }) => { + expect(escapeDartKeyword(config, blockName)).toBe('k_void'); + expect(buildBlockName(config, blockName, undefined, casing, decorateWithAtJsonKey)).toBe( + casing === 'camelCase' ? `@JsonKey(name: 'void') kVoid` : expected + ); + expect(buildBlockName(config, blockName, undefined, 'PascalCase', decorateWithAtJsonKey)).toBe( + `@JsonKey(name: 'void') KVoid` + ); + }); + + test.each(data[6].args)(data[6].title, ({ config, blockName, casing, expected }) => { + expect(escapeDartKeyword(config, blockName)).toBe(expected); + expect(buildBlockName(config, blockName, undefined, casing)).toBe(casing === 'camelCase' ? 'voidK' : expected); + expect(buildBlockName(config, blockName, undefined, 'PascalCase')).toBe('VoidK'); }); - test('transformCustomDecorators()', () => { - expect(transformCustomDecorators(globalCustomDecorators)).toMatchObject([ - '@JsonSerializable(explicitToJson: true)\n', - ]); + test.each(data[7].args)(data[7].title, ({ config, blockName, casing, decorateWithAtJsonKey, expected }) => { + expect(escapeDartKeyword(config, blockName)).toBe('void_k'); + expect(buildBlockName(config, blockName, undefined, casing, decorateWithAtJsonKey)).toBe( + casing === 'camelCase' ? `@JsonKey(name: 'void') voidK` : expected + ); + expect(buildBlockName(config, blockName, undefined, 'PascalCase', decorateWithAtJsonKey)).toBe( + `@JsonKey(name: 'void') VoidK` + ); + }); + + test.each(data[8].args)(data[8].title, ({ config, blockName, casing, expected }) => { + expect(escapeDartKeyword(config, blockName)).toBe(expected); + expect(buildBlockName(config, blockName, undefined, casing)).toBe(casing === 'camelCase' ? 'kVoidK' : expected); + expect(buildBlockName(config, blockName, undefined, 'PascalCase')).toBe('KVoidK'); + }); + + test.each(data[9].args)(data[9].title, ({ config, blockName, casing, decorateWithAtJsonKey, expected }) => { + expect(escapeDartKeyword(config, blockName)).toBe('k_void_k'); + expect(buildBlockName(config, blockName, undefined, casing, decorateWithAtJsonKey)).toBe( + casing === 'camelCase' ? `@JsonKey(name: 'void') kVoidK` : expected + ); + expect(buildBlockName(config, blockName, undefined, 'PascalCase', decorateWithAtJsonKey)).toBe( + `@JsonKey(name: 'void') KVoidK` + ); + }); + }); + + describe('block builders => ', () => { + const config = mergeConfig(); + + const validBlockNames = [ + 'Episode', + 'Movie', + 'CreateMovieInput', + 'UpsertMovieInput', + 'UpdateMovieInput', + 'DeleteMovieInput', + 'Starship', + 'Character', + 'MovieCharacter', + 'Human', + 'Droid', + 'SearchResult', + ]; + + test.each(validBlockNames)('returns a blockHeader', blockName => { + expect(buildEnumHeader(config, blockName)).toBe(`enum ${blockName} {\n`); + + const privateEmptyConstructor = indent(`const ${blockName}._();\n\n`); + expect(buildClassHeader(config, blockName)).toBe( + `class ${blockName} with _$${blockName} {\n${privateEmptyConstructor}` + ); + expect(buildClassHeader(config, blockName, false)).toBe(`class ${blockName} with _$${blockName} {\n`); - /* - expect(transformCustomDecorators(droidCustomDecorators, 'Droid')).toMatchObject([ - '@JsonSerializable(explicitToJson: true)\n', - "@FreezedUnionValue('BestDroid')\n", - ]); + const fromJsonToJson = indent( + `factory ${blockName}.fromJson(Map json) => _$${blockName}FromJson(json);\n}\n\n` + ); + expect(buildClassFooter(config, blockName)).toBe(fromJsonToJson); + expect(buildClassFooter(config, blockName, false)).toBe(`}\n\n`); + }); + + expect(buildEnumFooter()).toBe(`}\n\n`); + + describe('method: buildBlock() => enumBlock', () => { + const node = astNodesList[0] as NodeType; + const expected = [ + `enum Episode {`, + indent(`@JsonKey(name: 'NEWHOPE') newhope,`), + indent(`@JsonKey(name: 'EMPIRE') empire,`), + indent(`@JsonKey(name: 'JEDI') jedi,`), + indent(`@JsonKey(name: 'VOID') void_,`), + indent(`@JsonKey(name: 'void') void_,`), + indent(`@JsonKey(name: 'IN') in_,`), + indent(`@JsonKey(name: 'in') in_,`), + indent(`@JsonKey(name: 'ELSE') else_,`), + indent(`@JsonKey(name: 'else') else_,`), + indent(`@JsonKey(name: 'SWITCH') switch_,`), + indent(`@JsonKey(name: 'switch') switch_,`), + indent(`@JsonKey(name: 'FACTORY') factory_,`), + indent(`@JsonKey(name: 'factory') factory_,`), + ]; + expect(buildBlock(config, node, new NodeRepository())).toBe( + expected.concat([indent(`male,`), indent(`female,`), indent(`phoneNumber,`), `}\n\n`]).join('\n') + ); + + expect( + buildBlock( + mergeConfig(config, { globalFreezedConfig: { alwaysUseJsonKeyName: true } }), + node, + new NodeRepository() + ) + ).toBe( + expected + .concat([ + indent(`@JsonKey(name: 'male') male,`), + indent(`@JsonKey(name: 'female') female,`), + indent(`@JsonKey(name: 'phoneNumber') phoneNumber,`), + `}\n\n`, + ]) + .join('\n') + ); + }); - expect(transformCustomDecorators(idCustomDecorators, 'Droid', 'id')).toMatchObject([ - '@NanoId(size: 16, alphabets: NanoId.ALPHA_NUMERIC)\n', - ]); - */ + describe('method: buildBlock() => classBlock', () => { + const config = mergeConfig(); + const node = astNodesList[1] as NodeType; + const nodeRepository = new NodeRepository(); + const expected = [ + `@freezed`, + `class Movie with _$Movie {`, + indent(`const Movie._();\n`), + indent(`==>factory==>Movie`), + ]; + + expect(buildBlock(config, node, nodeRepository)).toBe( + expected + .concat([indent(`factory Movie.fromJson(Map json) => _$MovieFromJson(json);`), `}\n\n`]) + .join('\n') + ); + + expect( + buildBlock(mergeConfig(config, { globalFreezedConfig: { fromJsonToJson: false } }), node, new NodeRepository()) + ).toBe(expected.concat([`}\n\n`]).join('\n')); + + describe('method: buildBlock() => factoryBlock', () => { + const config = mergeConfig(); + // const placeholder = indent(`==>factory==>Movie\n`); + // const blockName = 'Movie'; // TODO: get blockName from placeholder + + const node = nodeRepository.get('Movie'); + const expected = [`const factory Movie({`]; + if (node) { + expect(FreezedFactoryBlock.buildFromFactory(config, node)).toBe( + expected + .concat([indent(`required String id,`, 2), indent(`required String title,`, 2), `}) = _Movie;\n`]) + .join('\n') + ); + } + }); }); }); }); From 8171c9c55b40130409b11761bf7e10d899cb6101 Mon Sep 17 00:00:00 2001 From: parables-amalitech Date: Wed, 30 Nov 2022 04:44:39 +0000 Subject: [PATCH 3/3] fix esm module imports errors --- .../src/freezed-declaration-blocks/class-block.ts | 4 ++-- .../src/freezed-declaration-blocks/factory-block.ts | 4 ++-- .../flutter-freezed/src/freezed-declaration-blocks/index.ts | 6 +++--- .../src/freezed-declaration-blocks/parameter-block.ts | 2 +- packages/plugins/dart/flutter-freezed/src/index.ts | 4 ++-- packages/plugins/dart/flutter-freezed/src/schema-visitor.ts | 4 ++-- packages/plugins/dart/flutter-freezed/src/utils.ts | 6 +++--- packages/plugins/dart/flutter-freezed/tests/config.spec.ts | 6 +++--- packages/plugins/dart/flutter-freezed/tests/config.ts | 4 ++-- packages/plugins/dart/flutter-freezed/tests/utils.spec.ts | 4 ++-- 10 files changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/class-block.ts b/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/class-block.ts index 61ab7a790..2e767836a 100644 --- a/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/class-block.ts +++ b/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/class-block.ts @@ -1,5 +1,5 @@ import { Kind } from 'graphql'; -import { FlutterFreezedPluginConfig } from '../config'; +import { FlutterFreezedPluginConfig } from '../config.js'; import { buildBlockComment, buildBlockDecorators, @@ -7,7 +7,7 @@ import { buildBlockBody, buildBlockFooter, NodeType, -} from '../utils'; +} from '../utils.js'; export class FreezedDeclarationBlock { public static build(config: FlutterFreezedPluginConfig, node: NodeType): string { diff --git a/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/factory-block.ts b/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/factory-block.ts index 1b2e2c47b..b7bdf61ad 100644 --- a/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/factory-block.ts +++ b/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/factory-block.ts @@ -1,5 +1,5 @@ import { indent } from '@graphql-codegen/visitor-plugin-common'; -import { FlutterFreezedPluginConfig } from '../config'; +import { FlutterFreezedPluginConfig } from '../config.js'; import { // buildBlockComment, // buildBlockDecorators, @@ -7,7 +7,7 @@ import { buildBlockBody, buildBlockFooter, NodeType, -} from '../utils'; +} from '../utils.js'; export class FreezedFactoryBlock { public static build( diff --git a/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/index.ts b/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/index.ts index fc3651b67..160c8dd83 100644 --- a/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/index.ts +++ b/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/index.ts @@ -1,3 +1,3 @@ -export * from './parameter-block'; -export * from './factory-block'; -export * from './class-block'; +export * from './parameter-block.js'; +export * from './factory-block.js'; +export * from './class-block.js'; diff --git a/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/parameter-block.ts b/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/parameter-block.ts index 4106c69c1..ab7300ba4 100644 --- a/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/parameter-block.ts +++ b/packages/plugins/dart/flutter-freezed/src/freezed-declaration-blocks/parameter-block.ts @@ -1,4 +1,4 @@ -import { FlutterFreezedPluginConfig } from '../config'; +import { FlutterFreezedPluginConfig } from '../config.js'; import { // buildBlockComment, // buildBlockDecorators, diff --git a/packages/plugins/dart/flutter-freezed/src/index.ts b/packages/plugins/dart/flutter-freezed/src/index.ts index c7469c69d..5da03703d 100644 --- a/packages/plugins/dart/flutter-freezed/src/index.ts +++ b/packages/plugins/dart/flutter-freezed/src/index.ts @@ -1,9 +1,9 @@ import { oldVisit, PluginFunction, Types } from '@graphql-codegen/plugin-helpers'; import { transformSchemaAST } from '@graphql-codegen/schema-ast'; import { GraphQLSchema } from 'graphql'; -import { FlutterFreezedPluginConfig } from './config'; +import { FlutterFreezedPluginConfig } from './config.js'; import { schemaVisitor } from './schema-visitor'; -import { buildImportStatements, defaultFreezedPluginConfig } from './utils'; +import { buildImportStatements, defaultFreezedPluginConfig } from './utils.js'; export const plugin: PluginFunction = ( schema: GraphQLSchema, diff --git a/packages/plugins/dart/flutter-freezed/src/schema-visitor.ts b/packages/plugins/dart/flutter-freezed/src/schema-visitor.ts index f5abf57bc..b7a25071c 100644 --- a/packages/plugins/dart/flutter-freezed/src/schema-visitor.ts +++ b/packages/plugins/dart/flutter-freezed/src/schema-visitor.ts @@ -5,8 +5,8 @@ import { ObjectTypeDefinitionNode, UnionTypeDefinitionNode, } from 'graphql'; -import { FlutterFreezedPluginConfig } from './config'; -import { buildBlock, NodeRepository } from './utils'; +import { FlutterFreezedPluginConfig } from './config.js'; +import { buildBlock, NodeRepository } from './utils.js'; export const schemaVisitor = (_schema: GraphQLSchema, config: FlutterFreezedPluginConfig) => { const nodeRepository = new NodeRepository(); diff --git a/packages/plugins/dart/flutter-freezed/src/utils.ts b/packages/plugins/dart/flutter-freezed/src/utils.ts index 70982d69a..685a37603 100644 --- a/packages/plugins/dart/flutter-freezed/src/utils.ts +++ b/packages/plugins/dart/flutter-freezed/src/utils.ts @@ -25,9 +25,9 @@ import { DART_SCALARS, DartIdentifierCasing, DART_KEYWORDS, -} from './config'; -import { FreezedDeclarationBlock, FreezedFactoryBlock } from './freezed-declaration-blocks'; -import { FreezedParameterBlock } from './freezed-declaration-blocks/parameter-block'; +} from './config.js'; +import { FreezedDeclarationBlock, FreezedFactoryBlock } from './freezed-declaration-blocks/index.js'; +import { FreezedParameterBlock } from './freezed-declaration-blocks/parameter-block.js'; export type FreezedConfigOptionName = keyof FreezedConfig; diff --git a/packages/plugins/dart/flutter-freezed/tests/config.spec.ts b/packages/plugins/dart/flutter-freezed/tests/config.spec.ts index 6881004fc..9aba29cff 100644 --- a/packages/plugins/dart/flutter-freezed/tests/config.spec.ts +++ b/packages/plugins/dart/flutter-freezed/tests/config.spec.ts @@ -1,6 +1,6 @@ -import { DART_KEYWORDS, DART_SCALARS, FlutterFreezedPluginConfig } from '../src/config'; -import { defaultFreezedConfig, defaultFreezedPluginConfig } from '../src/utils'; -import { customDecoratorsConfig, typeConfig } from './config'; +import { DART_KEYWORDS, DART_SCALARS, FlutterFreezedPluginConfig } from '../src/config.js'; +import { defaultFreezedConfig, defaultFreezedPluginConfig } from '../src/utils.js'; +import { customDecoratorsConfig, typeConfig } from './config.js'; describe('flutter-freezed-plugin-config', () => { it('should return the built-in Dart scalar types', () => { diff --git a/packages/plugins/dart/flutter-freezed/tests/config.ts b/packages/plugins/dart/flutter-freezed/tests/config.ts index d4cbae6ee..78429da0c 100644 --- a/packages/plugins/dart/flutter-freezed/tests/config.ts +++ b/packages/plugins/dart/flutter-freezed/tests/config.ts @@ -1,5 +1,5 @@ -import { FlutterFreezedPluginConfig } from '../src/config'; -import { mergeConfig } from '../src/utils'; +import { FlutterFreezedPluginConfig } from '../src/config.js'; +import { mergeConfig } from '../src/utils.js'; export const typeConfig: FlutterFreezedPluginConfig = mergeConfig({ globalFreezedConfig: { diff --git a/packages/plugins/dart/flutter-freezed/tests/utils.spec.ts b/packages/plugins/dart/flutter-freezed/tests/utils.spec.ts index 76681bd66..dbc6fc564 100644 --- a/packages/plugins/dart/flutter-freezed/tests/utils.spec.ts +++ b/packages/plugins/dart/flutter-freezed/tests/utils.spec.ts @@ -1,7 +1,7 @@ import { transformSchemaAST } from '@graphql-codegen/schema-ast'; import { indent } from '@graphql-codegen/visitor-plugin-common'; -import { DartIdentifierCasing, FlutterFreezedPluginConfig } from '../src/config'; -import { FreezedFactoryBlock } from '../src/freezed-declaration-blocks'; +import { DartIdentifierCasing, FlutterFreezedPluginConfig } from '../src/config.js'; +import { FreezedFactoryBlock } from '../src/freezed-declaration-blocks/index.js'; import { buildBlock, buildBlockName,