graphql-codegen-flutter-freezed
Version:
A stand-alone package to generate Freezed models from GraphQL schema based on the flutter-freezed plugin for GraphQL Code Generator
224 lines (223 loc) • 9.95 kB
JavaScript
import { indent } from '@graphql-codegen/visitor-plugin-common';
import { Kind } from 'graphql';
import { camelCase, pascalCase } from 'change-case-all';
import { getCustomDecorators, transformCustomDecorators, FreezedConfigValue, } from '../utils';
import { FreezedFactoryBlock } from './factory-block';
export class FreezedDeclarationBlock {
_config;
_freezedFactoryBlockRepository;
_node;
/** document the class */
_comment = '';
/** a list of decorators to copy paste to the generator */
_decorators = [];
/** the name of the class */
_name;
/** a list of default constructor and named Constructors used create a Freezed union/sealed class */
_factoryBlocks = [];
/** the shape is the content of the block */
_shape;
/** the block is the final structure that is generated */
_block;
_freezedConfigValue;
constructor(_config, _freezedFactoryBlockRepository, _node) {
this._config = _config;
this._freezedFactoryBlockRepository = _freezedFactoryBlockRepository;
this._node = _node;
this._config = _config;
this._freezedFactoryBlockRepository = _freezedFactoryBlockRepository;
this._node = _node;
this._freezedConfigValue = new FreezedConfigValue(this._config, this._node.name.value);
}
init() {
if (this._node.kind === Kind.ENUM_TYPE_DEFINITION) {
this.setDecorators().setName().setShape().setBlock();
return this;
}
this.setComment().setDecorators().setName().setFactoryBlocks().setShape().setBlock();
return this;
}
setComment() {
const comment = this._node.description?.value;
if (comment && comment !== null && comment !== '') {
this._comment = `/// ${comment} \n`;
}
return this;
}
getEnumComment(value) {
const comment = value.description?.value;
if (comment && comment !== null && comment !== '') {
return `/// ${comment} \n`;
}
return '';
}
setDecorators() {
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'];
}
return this;
}
getFreezedDecorator() {
const use_unfreezed = () => {
if (!this._freezedConfigValue.get('immutable') ||
(this._node.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION && this._freezedConfigValue.get('mutableInputs'))) {
return '@unfreezed';
}
return use_Freezed_or_freezed();
};
const use_Freezed_or_freezed = () => {
// if any of these options is not null, use the @Freezed() decorator passing in that option
const [isCustomized, copyWith, equal, makeCollectionsUnmodifiable, unionKey, unionValueCase] = isCustomizedFreezed();
if (isCustomized) {
return ``;
}
// else fallback to the normal @freezed decorator
return '@freezed';
};
const 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('unionValueCase');
const isCustomized = copyWith || equal || makeCollectionsUnmodifiable || unionKey || unionValueCase;
return [isCustomized, copyWith, equal, makeCollectionsUnmodifiable, unionKey, unionValueCase];
};
// this is the start of the pipeline of decisions to determine which Freezed decorator to use
return use_unfreezed();
}
setName() {
this._name = pascalCase(this._node.name.value);
return this;
}
setFactoryBlocks() {
if (this._node.kind === Kind.UNION_TYPE_DEFINITION) {
this._factoryBlocks =
this._node.types?.map((_type) => 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) => this._freezedFactoryBlockRepository.register(this._node.name.value, new FreezedFactoryBlock(this._config, this._node).init())) ?? [];
}
return this;
}
setShape() {
let shape = '';
// some helper variables
const name = this._node.name.value;
let namedConstructor;
let factoryBlockKey;
// handle enums differently
if (this._node.kind === Kind.ENUM_TYPE_DEFINITION) {
this._shape = this._node.values
?.map((value) => {
shape = indent(this.getEnumComment(value));
if (this._config.lowercaseEnums ?? true) {
shape += ` ${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 => {
namedConstructor = camelCase(input.split('$').join('_'));
factoryBlockKey = input.replace('$', name);
shape += `==>factory==>${factoryBlockKey}==>${'union_factory'}==>${name}==>${namedConstructor}\n`;
});
}
}
this._shape = shape;
return this;
}
/**
* returns the string output of the block
*/
setBlock() {
let block = '';
//append comment
block += this._comment;
// append the decorators
block += this._decorators.join('\n');
if (this._decorators !== []) {
block += '\n';
}
// 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<String, Object?> json) => _${this._name}FromJson(json);\n`);
}
//append end of class definition
block += '}\n\n';
this._block = block;
return this;
}
/** returns the block */
toString() {
if (!this._block) {
throw new Error('setShape must be called before calling toString()');
}
return this._block;
}
}