Skip to content

Commit

Permalink
chore: refactored dart generator (#778)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonaslagoni authored Jun 28, 2022
1 parent 73d7941 commit ce925c9
Show file tree
Hide file tree
Showing 12 changed files with 303 additions and 243 deletions.
74 changes: 74 additions & 0 deletions src/generators/dart/DartConstrainer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { TypeMapping } from '../../helpers';
import { defaultEnumKeyConstraints, defaultEnumValueConstraints } from './constrainer/EnumConstrainer';
import { defaultModelNameConstraints } from './constrainer/ModelNameConstrainer';
import { defaultPropertyKeyConstraints } from './constrainer/PropertyKeyConstrainer';
import { DartOptions } from './DartGenerator';

export const DartDefaultTypeMapping: TypeMapping<DartOptions> = {
Object ({constrainedModel}): string {
return constrainedModel.name;
},
Reference ({constrainedModel}): string {
return constrainedModel.name;
},
Any (): string {
return 'Object';
},
Float (): string {
return 'double';
},
Integer (): string {
return 'int';
},
String ({constrainedModel}): string {
const format = constrainedModel.originalInput['format'];
switch (format) {
case 'date':
return 'DateTime';
case 'time':
return 'DateTime';
case 'dateTime':
case 'date-time':
return 'DateTime';
case 'string':
case 'password':
case 'byte':
return 'String';
case 'binary':
return 'byte[]';
default: return 'string';
}
},
Boolean (): string {
return 'bool';
},
Tuple ({options}): string {
//Since Dart dont support tuples, lets use the most generic type
if (options.collectionType && options.collectionType === 'List') {
return 'List<Object>';
}
return 'Object[]';
},
Array ({constrainedModel, options}): string {
if (options.collectionType && options.collectionType === 'List') {
return `List<${constrainedModel.valueModel.type}>`;
}
return `${constrainedModel.valueModel.type}[]`;
},
Enum ({constrainedModel}): string {
return constrainedModel.name;
},
Union (): string {
return 'Object';
},
Dictionary ({constrainedModel}): string {
return `Map<${constrainedModel.key.type}, ${constrainedModel.value.type}>`;
}
};

export const DartDefaultConstraints = {
enumKey: defaultEnumKeyConstraints(),
enumValue: defaultEnumValueConstraints(),
modelName: defaultModelNameConstraints(),
propertyKey: defaultPropertyKeyConstraints()
};
4 changes: 2 additions & 2 deletions src/generators/dart/DartFileGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DartGenerator, DartRenderCompleteModelOptions } from './';
import { CommonInputModel, OutputModel } from '../../models';
import { InputMetaModel, OutputModel } from '../../models';
import * as path from 'path';
import { AbstractFileGenerator } from '../AbstractFileGenerator';
import { FileHelpers } from '../../helpers';
Expand All @@ -12,7 +12,7 @@ export class DartFileGenerator extends DartGenerator implements AbstractFileGene
* @param outputDirectory where you want the models generated to
* @param options
*/
public async generateToFiles(input: Record<string, unknown> | CommonInputModel, outputDirectory: string, options: DartRenderCompleteModelOptions): Promise<OutputModel[]> {
public async generateToFiles(input: Record<string, unknown> | InputMetaModel, outputDirectory: string, options: DartRenderCompleteModelOptions): Promise<OutputModel[]> {
let generatedModels = await this.generateCompleteModels(input, options);
generatedModels = generatedModels.filter((outputModel) => { return outputModel.modelName !== undefined; });
for (const outputModel of generatedModels) {
Expand Down
65 changes: 41 additions & 24 deletions src/generators/dart/DartGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@ import {
CommonGeneratorOptions,
defaultGeneratorOptions
} from '../AbstractGenerator';
import {CommonModel, CommonInputModel, RenderOutput} from '../../models';
import {CommonNamingConvention, CommonNamingConventionImplementation, ModelKind, TypeHelpers} from '../../helpers';
import {RenderOutput, ConstrainedMetaModel, MetaModel, ConstrainedObjectModel, ConstrainedEnumModel, InputMetaModel} from '../../models';
import {CommonNamingConvention, CommonNamingConventionImplementation, constrainMetaModel, Constraints, split, TypeMapping} from '../../helpers';
import {DartPreset, DART_DEFAULT_PRESET} from './DartPreset';
import {ClassRenderer} from './renderers/ClassRenderer';
import {EnumRenderer} from './renderers/EnumRenderer';
import {isReservedDartKeyword} from './Constants';
import {Logger} from '../../';
import {FormatHelpers} from '../../helpers/FormatHelpers';
import { DartDefaultConstraints, DartDefaultTypeMapping } from './DartConstrainer';

export interface DartOptions extends CommonGeneratorOptions<DartPreset> {
collectionType?: 'List';
namingConvention?: CommonNamingConvention;
typeMapping: TypeMapping<DartOptions>;
constraints: Constraints;
}

export interface DartRenderCompleteModelOptions {
Expand All @@ -26,30 +29,51 @@ export class DartGenerator extends AbstractGenerator<DartOptions, DartRenderComp
...defaultGeneratorOptions,
defaultPreset: DART_DEFAULT_PRESET,
collectionType: 'List',
namingConvention: CommonNamingConventionImplementation
namingConvention: CommonNamingConventionImplementation,
typeMapping: DartDefaultTypeMapping,
constraints: DartDefaultConstraints
};

constructor(
options: DartOptions = DartGenerator.defaultOptions,
) {
super('Dart', DartGenerator.defaultOptions, options);
const realizedOptions = {...DartGenerator.defaultOptions, ...options};
super('Dart', realizedOptions);
}

splitMetaModel(model: MetaModel): MetaModel[] {
//These are the models that we have separate renderers for
const metaModelsToSplit = {
splitEnum: true,
splitObject: true
};
return split(model, metaModelsToSplit);
}

constrainToMetaModel(model: MetaModel): ConstrainedMetaModel {
return constrainMetaModel(
this.options.typeMapping,
this.options.constraints,
{
metaModel: model,
options: this.options,
constrainedName: '' //This is just a placeholder, it will be constrained within the function
}
);
}
/**
* Render a scattered model, where the source code and library and model dependencies are separated.
*
* @param model
* @param inputModel
*/
render(model: CommonModel, inputModel: CommonInputModel): Promise<RenderOutput> {
const kind = TypeHelpers.extractKind(model);
// We don't support union in Dart generator, however, if union is an object, we render it as a class.
if (kind === ModelKind.OBJECT || (kind === ModelKind.UNION && model.type?.includes('object'))) {
render(model: ConstrainedMetaModel, inputModel: InputMetaModel): Promise<RenderOutput> {
if (model instanceof ConstrainedObjectModel) {
return this.renderClass(model, inputModel);
} else if (kind === ModelKind.ENUM) {
} else if (model instanceof ConstrainedEnumModel) {
return this.renderEnum(model, inputModel);
}
Logger.warn(`Dart generator, cannot generate this type of model, ${model.$id}`);
}
Logger.warn(`Dart generator, cannot generate this type of model, ${model.name}`);
return Promise.resolve(RenderOutput.toRenderOutput({result: '', renderedName: '', dependencies: []}));
}

Expand All @@ -62,19 +86,14 @@ export class DartGenerator extends AbstractGenerator<DartOptions, DartRenderComp
* @param inputModel
* @param options used to render the full output
*/
async renderCompleteModel(model: CommonModel, inputModel: CommonInputModel, options: DartRenderCompleteModelOptions): Promise<RenderOutput> {
async renderCompleteModel(model: ConstrainedMetaModel, inputModel: InputMetaModel, options: DartRenderCompleteModelOptions): Promise<RenderOutput> {
if (isReservedDartKeyword(options.packageName)) {
throw new Error(`You cannot use reserved Dart keyword (${options.packageName}) as package name, please use another.`);
}

const outputModel = await this.render(model, inputModel);
const modelDependencies = model.getNearestDependencies().map((dependencyModelName) => {
const formattedDependencyModelName = this.options.namingConvention?.type ? this.options.namingConvention.type(dependencyModelName, {
inputModel,
model: inputModel.models[String(dependencyModelName)],
reservedKeywordCallback: isReservedDartKeyword
}) : dependencyModelName;
return `import 'package:${options.packageName}/${FormatHelpers.snakeCase(formattedDependencyModelName)}.dart';`;
return `import 'package:${options.packageName}/${FormatHelpers.snakeCase(dependencyModelName.name)}.dart';`;
});
const outputContent = `${modelDependencies.join('\n')}
${outputModel.dependencies.join('\n')}
Expand All @@ -86,19 +105,17 @@ export class DartGenerator extends AbstractGenerator<DartOptions, DartRenderComp
});
}

async renderClass(model: CommonModel, inputModel: CommonInputModel): Promise<RenderOutput> {
async renderClass(model: ConstrainedObjectModel, inputModel: InputMetaModel): Promise<RenderOutput> {
const presets = this.getPresets('class');
const renderer = new ClassRenderer(this.options, this, presets, model, inputModel);
const result = await renderer.runSelfPreset();
const renderedName = FormatHelpers.snakeCase(renderer.nameType(model.$id, model));
return RenderOutput.toRenderOutput({result, renderedName, dependencies: renderer.dependencies});
return RenderOutput.toRenderOutput({result, renderedName: model.name, dependencies: renderer.dependencies});
}

async renderEnum(model: CommonModel, inputModel: CommonInputModel): Promise<RenderOutput> {
async renderEnum(model: ConstrainedEnumModel, inputModel: InputMetaModel): Promise<RenderOutput> {
const presets = this.getPresets('enum');
const renderer = new EnumRenderer(this.options, this, presets, model, inputModel);
const result = await renderer.runSelfPreset();
const renderedName = FormatHelpers.snakeCase(renderer.nameType(model.$id, model));
return RenderOutput.toRenderOutput({result, renderedName, dependencies: renderer.dependencies});
return RenderOutput.toRenderOutput({result, renderedName: model.name, dependencies: renderer.dependencies});
}
}
10 changes: 7 additions & 3 deletions src/generators/dart/DartPreset.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { Preset, ClassPreset, EnumPreset } from '../../models';
import { DartOptions } from './DartGenerator';
import { ClassRenderer, DART_DEFAULT_CLASS_PRESET } from './renderers/ClassRenderer';
import { EnumRenderer, DART_DEFAULT_ENUM_PRESET } from './renderers/EnumRenderer';

export type DartPreset = Preset<{
class: ClassPreset<ClassRenderer>;
enum: EnumPreset<EnumRenderer>;
export type ClassPresetType<O> = ClassPreset<ClassRenderer, O>;
export type EnumPresetType<O> = EnumPreset<EnumRenderer, O>;

export type DartPreset<O = DartOptions> = Preset<{
class: ClassPresetType<O>;
enum: EnumPresetType<O>;
}>;

export const DART_DEFAULT_PRESET: DartPreset = {
Expand Down
126 changes: 3 additions & 123 deletions src/generators/dart/DartRenderer.ts
Original file line number Diff line number Diff line change
@@ -1,143 +1,23 @@
import { AbstractRenderer } from '../AbstractRenderer';
import { DartGenerator, DartOptions } from './DartGenerator';
import { CommonModel, CommonInputModel, Preset } from '../../models';
import { FormatHelpers, ModelKind, TypeHelpers } from '../../helpers';
import { isReservedDartKeyword } from './Constants';
import { Preset, ConstrainedMetaModel } from '../../models';

/**
* Common renderer for Dart types
*
* @extends AbstractRenderer
*/
export abstract class DartRenderer extends AbstractRenderer<DartOptions, DartGenerator> {
export abstract class DartRenderer<RendererModelType extends ConstrainedMetaModel> extends AbstractRenderer<DartOptions, DartGenerator, RendererModelType> {
constructor(
options: DartOptions,
generator: DartGenerator,
presets: Array<[Preset, unknown]>,
model: CommonModel,
model: RendererModelType,
inputModel: CommonInputModel,
) {
super(options, generator, presets, model, inputModel);
}

/**
* Renders the name of a type based on provided generator option naming convention type function.
*
* This is used to render names of models and then later used if it is referenced from other models.
*
* @param name
* @param model
*/
nameType(name: string | undefined, model?: CommonModel): string {
return this.options?.namingConvention?.type
? this.options.namingConvention.type(name, { model: model || this.model, inputModel: this.inputModel, reservedKeywordCallback: isReservedDartKeyword })
: name || '';
}

/**
* Renders the name of a property based on provided generator option naming convention property function.
*
* @param propertyName
* @param property
*/
nameProperty(propertyName: string | undefined, property?: CommonModel): string {
return this.options?.namingConvention?.property
? this.options.namingConvention.property(propertyName, { model: this.model, inputModel: this.inputModel, property, reservedKeywordCallback: isReservedDartKeyword })
: propertyName || '';
}

/**
* Renders model(s) to Dart type(s).
*
* @param model
*/
renderType(model: CommonModel | CommonModel[]): string {
if (Array.isArray(model) || Array.isArray(model.type)) {
return 'Object'; // fallback
}
if (model.$ref !== undefined) {
return this.nameType(model.$ref, model);
}
const kind = TypeHelpers.extractKind(model);
if (
kind === ModelKind.PRIMITIVE ||
kind === ModelKind.ARRAY
) {
const format = model.getFromOriginalInput('format');
return this.toClassType(this.toDartType(format || model.type, model));
}
return this.nameType(model.$id, model);
}

/**
* Returns the Dart corresponding type from CommonModel type or JSON schema format
* @param type
* @param model
*/
toDartType(type: string | undefined, model: CommonModel): string {
switch (type) {
case 'integer':
case 'int32':
case 'long':
case 'int64':
return 'int';
case 'boolean':
return 'bool';
case 'date':
return 'DateTime';
case 'time':
return 'DateTime';
case 'dateTime':
case 'date-time':
return 'DateTime';
case 'string':
case 'password':
case 'byte':
return 'String';
case 'float':
case 'double':
case 'number':
return 'double';
case 'binary':
return 'byte[]';
case 'array': {
let arrayItemModel = model.items;
//Since Dart dont support tuples, lets make sure that we combine the tuple types to find the appropriate array type
if (Array.isArray(model.items)) {
arrayItemModel = model.items.reduce((prevModel, currentModel) => {
return CommonModel.mergeCommonModels(CommonModel.toCommonModel(prevModel), CommonModel.toCommonModel(currentModel), {});
});
//If tuples and additionalItems make sure to find the appropriate type by merging all the tuples and additionalItems model together to find the combined type.
if (model.additionalItems !== undefined) {
arrayItemModel = CommonModel.mergeCommonModels(arrayItemModel, model.additionalItems, {});
}
}
const newType = arrayItemModel ? this.renderType(arrayItemModel) : 'Object';
if (this.options.collectionType && this.options.collectionType === 'List') {
return `List<${newType}>`;
}
return `${newType}[]`;
}
default:
return 'Object';
}
}

toClassType(type: string): string {
switch (type) {
case 'int':
case 'long':
return 'int';
case 'boolean':
return 'bool';
case 'float':
case 'double':
return 'double';
default:
return `${type}`;
}
}

renderComments(lines: string | string[]): string {
lines = FormatHelpers.breakLines(lines);
const newLiteral = lines.map(line => ` * ${line}`).join('\n');
Expand Down
Loading

0 comments on commit ce925c9

Please sign in to comment.