diff --git a/packages/quicktype-core/src/ConvenienceRenderer.ts b/packages/quicktype-core/src/ConvenienceRenderer.ts index 2c9c6ff42..aef41b181 100644 --- a/packages/quicktype-core/src/ConvenienceRenderer.ts +++ b/packages/quicktype-core/src/ConvenienceRenderer.ts @@ -129,7 +129,7 @@ export abstract class ConvenienceRenderer extends Renderer { * that can conflict with that, such as reserved keywords or common type * names. */ - protected forbiddenNamesForGlobalNamespace(): string[] { + protected forbiddenNamesForGlobalNamespace(): readonly string[] { return []; } diff --git a/packages/quicktype-core/src/Naming.ts b/packages/quicktype-core/src/Naming.ts index 6b2b2b825..cc98c50fc 100644 --- a/packages/quicktype-core/src/Naming.ts +++ b/packages/quicktype-core/src/Naming.ts @@ -319,7 +319,7 @@ export class DependencyName extends Name { } } -export function keywordNamespace(name: string, keywords: string[]): Namespace { +export function keywordNamespace(name: string, keywords: readonly string[]): Namespace { const ns = new Namespace(name, undefined, [], []); for (const kw of keywords) { ns.add(new FixedName(kw)); diff --git a/packages/quicktype-core/src/index.ts b/packages/quicktype-core/src/index.ts index fa1ea78be..6da647b12 100644 --- a/packages/quicktype-core/src/index.ts +++ b/packages/quicktype-core/src/index.ts @@ -84,35 +84,4 @@ export { removeNullFromUnion, matchType, nullableFromUnion } from "./TypeUtils"; export { ConvenienceRenderer } from "./ConvenienceRenderer"; export { uriTypeAttributeKind } from "./attributes/URIAttributes"; -export { CJSONTargetLanguage, CJSONRenderer, cJSONOptions } from "./language/CJSON"; -export { CPlusPlusTargetLanguage, CPlusPlusRenderer, cPlusPlusOptions } from "./language/CPlusPlus"; -export { CSharpTargetLanguage, cSharpOptions, CSharpRenderer } from "./language/CSharp"; -export { PythonTargetLanguage, PythonRenderer, pythonOptions } from "./language/Python"; -export { GoTargetLanguage, GoRenderer, goOptions } from "./language/Golang"; -export { ObjectiveCTargetLanguage, ObjectiveCRenderer, objcOptions } from "./language/Objective-C"; -export { JavaTargetLanguage, JavaRenderer, javaOptions } from "./language/Java"; -export { JavaScriptTargetLanguage, JavaScriptRenderer, javaScriptOptions } from "./language/JavaScript"; -export { - JavaScriptPropTypesTargetLanguage, - JavaScriptPropTypesRenderer, - javaScriptPropTypesOptions -} from "./language/JavaScriptPropTypes"; -export { - TypeScriptTargetLanguage, - TypeScriptRenderer, - FlowTargetLanguage, - FlowRenderer, - tsFlowOptions -} from "./language/TypeScriptFlow"; -export { SwiftTargetLanguage, SwiftRenderer, swiftOptions } from "./language/Swift"; -export { KotlinTargetLanguage, KotlinRenderer, kotlinOptions } from "./language/Kotlin"; -export { Scala3TargetLanguage, Scala3Renderer, scala3Options } from "./language/Scala3"; -export { SmithyTargetLanguage, Smithy4sRenderer, SmithyOptions } from "./language/Smithy4s"; -export { ElmTargetLanguage, ElmRenderer, elmOptions } from "./language/Elm"; -export { JSONSchemaTargetLanguage, JSONSchemaRenderer } from "./language/JSONSchema"; -export { RustTargetLanguage, RustRenderer, rustOptions } from "./language/Rust"; -export { RubyTargetLanguage, RubyRenderer, rubyOptions } from "./language/ruby"; -export { CrystalTargetLanguage, CrystalRenderer } from "./language/Crystal"; -export { HaskellTargetLanguage, HaskellRenderer, haskellOptions } from "./language/Haskell"; -export { DartTargetLanguage, DartRenderer, dartOptions } from "./language/Dart"; -export { ElixirTargetLanguage, ElixirRenderer, elixirOptions } from "./language/Elixir"; +export * from "./language"; diff --git a/packages/quicktype-core/src/language/All.ts b/packages/quicktype-core/src/language/All.ts index 297af4d1c..227ca4322 100644 --- a/packages/quicktype-core/src/language/All.ts +++ b/packages/quicktype-core/src/language/All.ts @@ -21,7 +21,7 @@ import { ObjectiveCTargetLanguage } from "./Objective-C"; import { PhpTargetLanguage } from "./Php"; import { PikeTargetLanguage } from "./Pike"; import { PythonTargetLanguage } from "./Python"; -import { RubyTargetLanguage } from "./ruby"; +import { RubyTargetLanguage } from "./Ruby"; import { RustTargetLanguage } from "./Rust"; import { Scala3TargetLanguage } from "./Scala3"; import { SmithyTargetLanguage } from "./Smithy4s"; @@ -31,33 +31,33 @@ import { FlowTargetLanguage, TypeScriptTargetLanguage } from "./TypeScriptFlow"; import { TypeScriptZodTargetLanguage } from "./TypeScriptZod"; export const all: TargetLanguage[] = [ - new CSharpTargetLanguage(), - new GoTargetLanguage(), - new RustTargetLanguage(), - new CrystalTargetLanguage(), new CJSONTargetLanguage(), new CPlusPlusTargetLanguage(), - new ObjectiveCTargetLanguage(), + new CrystalTargetLanguage(), + new CSharpTargetLanguage(), + new DartTargetLanguage(), + new ElixirTargetLanguage(), + new ElmTargetLanguage(), + new FlowTargetLanguage(), + new GoTargetLanguage(), + new HaskellTargetLanguage(), new JavaTargetLanguage(), - new TypeScriptTargetLanguage(), new JavaScriptTargetLanguage(), new JavaScriptPropTypesTargetLanguage(), - new FlowTargetLanguage(), - new SwiftTargetLanguage(), - new Scala3TargetLanguage(), - new SmithyTargetLanguage(), - new KotlinTargetLanguage(), - new ElmTargetLanguage(), new JSONSchemaTargetLanguage(), - new RubyTargetLanguage(), - new DartTargetLanguage(), - new PythonTargetLanguage("Python", ["python", "py"], "py"), + new KotlinTargetLanguage(), + new ObjectiveCTargetLanguage(), + new PhpTargetLanguage(), new PikeTargetLanguage(), - new HaskellTargetLanguage(), - new TypeScriptZodTargetLanguage(), + new PythonTargetLanguage("Python", ["python", "py"], "py"), + new RubyTargetLanguage(), + new RustTargetLanguage(), + new Scala3TargetLanguage(), + new SmithyTargetLanguage(), + new SwiftTargetLanguage(), + new TypeScriptTargetLanguage(), new TypeScriptEffectSchemaTargetLanguage(), - new ElixirTargetLanguage(), - new PhpTargetLanguage() + new TypeScriptZodTargetLanguage() ]; export function languageNamed(name: string, targetLanguages?: TargetLanguage[]): TargetLanguage | undefined { diff --git a/packages/quicktype-core/src/language/CJSON.ts b/packages/quicktype-core/src/language/CJSON/CJSONRenderer.ts similarity index 95% rename from packages/quicktype-core/src/language/CJSON.ts rename to packages/quicktype-core/src/language/CJSON/CJSONRenderer.ts index 28499a4b4..6f9dc33b9 100644 --- a/packages/quicktype-core/src/language/CJSON.ts +++ b/packages/quicktype-core/src/language/CJSON/CJSONRenderer.ts @@ -1,368 +1,32 @@ // FIXME: NEEDS REFACTOR /* eslint-disable @typescript-eslint/no-shadow */ /* eslint-disable @typescript-eslint/naming-convention */ -/** - * CJSON.ts - * This file is used to generate cJSON code with quicktype - * The generated code depends of https://github.com/DaveGamble/cJSON, https://github.com/joelguittet/c-list and https://github.com/joelguittet/c-hashtable - * - * Similarly to C++ generator, it is possible to generate a single header file or multiple header files. - * To generate multiple header files, use the following option: --source-style multi-source - * - * JSON data are represented using structures, and functions in the cJSON style are created to use them. - * To parse json data from json string use the following: struct * data = cJSON_Parse(); - * To get json data from cJSON object use the following: struct * data = cJSON_GetValue(); - * To get cJSON object from json data use the following: cJSON * cjson = cJSON_Create(); - * To print json string from json data use the following: char * string = cJSON_Print(); - * To delete json data use the following: cJSON_Delete(); - * - * TODO list for future enhancements: - * - Management of Class, Union and TopLevel should be mutualized to reduce code size and to permit Union and TopLevel having recursive Array/Map - * - Types check should be added to verify unwanted inputs (for example a Number passed while a String is expected, etc) - * - Constraints should be implemented (verification of Enum values, min/max values for Numbers and min/max length for Strings, regex) - * - Support of pure Any type for example providing a callback from the application to handle these cases dynamically - * See test/languages.ts for the test cases which are not implmented/checked. - */ - -/* Imports */ -import { getAccessorName } from "../attributes/AccessorNames"; -import { enumCaseValues } from "../attributes/EnumValues"; -import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../ConvenienceRenderer"; -import { type Name, type NameStyle, type Namer, funPrefixNamer } from "../Naming"; -import { type RenderContext } from "../Renderer"; -import { EnumOption, type Option, type OptionValues, StringOption, getOptionValues } from "../RendererOptions"; -import { type Sourcelike } from "../Source"; -import { - type NamingStyle, - allUpperWordStyle, - isAscii, - isLetterOrUnderscoreOrDigit, - legalizeCharacters, - makeNameStyle -} from "../support/Strings"; -import { assert, assertNever, defined, numberEnumValues, panic } from "../support/Support"; -import { TargetLanguage } from "../TargetLanguage"; -import { ArrayType, ClassType, EnumType, MapType, type Type, type TypeKind, UnionType } from "../Type"; -import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../types"; -import { matchType, nullableFromUnion, removeNullFromUnion } from "../TypeUtils"; - -/* Naming styles */ -const pascalValue: [string, NamingStyle] = ["pascal-case", "pascal"]; -const underscoreValue: [string, NamingStyle] = ["underscore-case", "underscore"]; -const camelValue: [string, NamingStyle] = ["camel-case", "camel"]; -const upperUnderscoreValue: [string, NamingStyle] = ["upper-underscore-case", "upper-underscore"]; -const pascalUpperAcronymsValue: [string, NamingStyle] = ["pascal-case-upper-acronyms", "pascal-upper-acronyms"]; -const camelUpperAcronymsValue: [string, NamingStyle] = ["camel-case-upper-acronyms", "camel-upper-acronyms"]; - -/* cJSON generator options */ -export const cJSONOptions = { - typeSourceStyle: new EnumOption( - "source-style", - "Source code generation type, whether to generate single or multiple source files", - [ - ["single-source", true], - ["multi-source", false] - ], - "single-source", - "secondary" - ), - typeIntegerSize: new EnumOption( - "integer-size", - "Integer code generation type (int64_t by default)", - [ - ["int8_t", "int8_t"], - ["int16_t", "int16_t"], - ["int32_t", "int32_t"], - ["int64_t", "int64_t"] - ], - "int64_t", - "secondary" - ), - hashtableSize: new StringOption( - "hashtable-size", - "Hashtable size, used when maps are created (64 by default)", - "SIZE", - "64" - ), - addTypedefAlias: new EnumOption( - "typedef-alias", - "Add typedef alias to unions, structs, and enums (no typedef by default)", - [ - ["no-typedef", false], - ["add-typedef", true] - ], - "no-typedef", - "secondary" - ), - printStyle: new EnumOption( - "print-style", - "Which cJSON print should be used (formatted by default)", - [ - ["print-formatted", false], - ["print-unformatted", true] - ], - "print-formatted", - "secondary" - ), - typeNamingStyle: new EnumOption("type-style", "Naming style for types", [ - pascalValue, - underscoreValue, - camelValue, - upperUnderscoreValue, - pascalUpperAcronymsValue, - camelUpperAcronymsValue - ]), - memberNamingStyle: new EnumOption("member-style", "Naming style for members", [ - underscoreValue, - pascalValue, - camelValue, - upperUnderscoreValue, - pascalUpperAcronymsValue, - camelUpperAcronymsValue - ]), - enumeratorNamingStyle: new EnumOption("enumerator-style", "Naming style for enumerators", [ - upperUnderscoreValue, - underscoreValue, - pascalValue, - camelValue, - pascalUpperAcronymsValue, - camelUpperAcronymsValue - ]) -}; - -/* cJSON generator target language */ -export class CJSONTargetLanguage extends TargetLanguage { - /** - * Constructor - * @param displayName: display name - * @params names: names - * @param extension: extension of files - */ - public constructor(displayName = "C (cJSON)", names: string[] = ["cjson", "cJSON"], extension = "h") { - super(displayName, names, extension); - } - - /** - * Return cJSON generator options - * @return cJSON generator options array - */ - protected getOptions(): Array> { - return [ - cJSONOptions.typeSourceStyle, - cJSONOptions.typeIntegerSize, - cJSONOptions.addTypedefAlias, - cJSONOptions.printStyle, - cJSONOptions.hashtableSize, - cJSONOptions.typeNamingStyle, - cJSONOptions.memberNamingStyle, - cJSONOptions.enumeratorNamingStyle - ]; - } - - /** - * Indicate if language support union with both number types - * @return true - */ - public get supportsUnionsWithBothNumberTypes(): boolean { - return true; - } - - /** - * Indicate if language support optional class properties - * @return true - */ - public get supportsOptionalClassProperties(): boolean { - return true; - } - - /** - * Create renderer - * @param renderContext: render context - * @param untypedOptionValues - * @return cJSON renderer - */ - protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): CJSONRenderer { - return new CJSONRenderer(this, renderContext, getOptionValues(cJSONOptions, untypedOptionValues)); - } -} - -/* Function used to format names */ -const legalizeName = legalizeCharacters(cp => isAscii(cp) && isLetterOrUnderscoreOrDigit(cp)); - -/* Forbidden names for namespace */ -const keywords = [ - /* C and C++ keywords */ - "alignas", - "alignof", - "and", - "and_eq", - "asm", - "atomic_cancel", - "atomic_commit", - "atomic_noexcept", - "auto", - "bitand", - "bitor", - "bool", - "break", - "case", - "catch", - "char", - "char16_t", - "char32_t", - "class", - "compl", - "concept", - "const", - "constexpr", - "const_cast", - "continue", - "co_await", - "co_return", - "co_yield", - "decltype", - "default", - "delete", - "do", - "double", - "dynamic_cast", - "else", - "enum", - "explicit", - "export", - "extern", - "false", - "float", - "for", - "friend", - "goto", - "if", - "import", - "inline", - "int", - "long", - "module", - "mutable", - "namespace", - "new", - "noexcept", - "not", - "not_eq", - "nullptr", - "operator", - "or", - "or_eq", - "private", - "protected", - "public", - "register", - "reinterpret_cast", - "requires", - "restrict", - "return", - "short", - "signed", - "sizeof", - "static", - "static_assert", - "static_cast", - "struct", - "switch", - "synchronized", - "template", - "this", - "thread_local", - "throw", - "true", - "try", - "typedef", - "typeid", - "typename", - "typeof", - "union", - "unsigned", - "using", - "virtual", - "void", - "volatile", - "wchar_t", - "while", - "xor", - "xor_eq", - "override", - "final", - "transaction_safe", - "transaction_safe_dynamic", - "NULL", - /* cJSON keywords */ - "Array", - "ArrayReference", - "Bool", - "DoubleArray", - "False", - "FloatArray", - "IntArray", - "Object", - "Null", - "Number", - "Raw", - "String", - "StringArray", - "StringReference", - "True" -]; - -/* Used to build forbidden global names */ -export enum GlobalNames { - ClassMemberConstraints = 1, - ClassMemberConstraintException = 2, - ValueTooLowException = 3, - ValueTooHighException = 4, - ValueTooShortException = 5, - ValueTooLongException = 6, - InvalidPatternException = 7, - CheckConstraint = 8 -} -/* To be able to support circles in multiple files - e.g. class#A using class#B using class#A (obviously not directly) we can forward declare them */ -export enum IncludeKind { - ForwardDeclare = "ForwardDeclare", - Include = "Include" -} - -/* Used to map includes */ -export interface IncludeRecord { - kind: IncludeKind | undefined /* How to include that */; - typeKind: TypeKind | undefined /* What exactly to include */; -} - -/* Used to map includes */ -export interface TypeRecord { - forceInclude: boolean; - level: number; - name: Name; - type: Type; - variant: boolean; -} - -/* Map each and every unique type to a include kind, e.g. how to include the given type */ -export type IncludeMap = Map; - -/* cJSON type */ -export interface TypeCJSON { - addToObject: Sourcelike /* cJSON add to object function */; - cType: Sourcelike /* C type */; - cjsonType: string /* cJSON type */; - createObject: Sourcelike /* cJSON create object function */; - deleteType: Sourcelike /* cJSON delete function */; - getValue: Sourcelike /* cJSON get value function */; - isNullable: boolean /* True if the field is nullable */; - isType: Sourcelike /* cJSON check type function */; - items: TypeCJSON | undefined /* Sub-items, used for arrays and map */; - optionalQualifier: string /* C optional qualifier, empty string if not defined */; -} +import { getAccessorName } from "../../attributes/AccessorNames"; +import { enumCaseValues } from "../../attributes/EnumValues"; +import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../../ConvenienceRenderer"; +import { type Name, type NameStyle, type Namer, funPrefixNamer } from "../../Naming"; +import { type RenderContext } from "../../Renderer"; +import { type OptionValues } from "../../RendererOptions"; +import { type Sourcelike } from "../../Source"; +import { type NamingStyle, allUpperWordStyle, makeNameStyle } from "../../support/Strings"; +import { assert, assertNever, defined, numberEnumValues, panic } from "../../support/Support"; +import { type TargetLanguage } from "../../TargetLanguage"; +import { ArrayType, ClassType, EnumType, MapType, type Type, UnionType } from "../../Type"; +import { matchType, nullableFromUnion, removeNullFromUnion } from "../../TypeUtils"; + +import { keywords } from "./constants"; +import { type cJSONOptions } from "./language"; +import { + GlobalNames, + IncludeKind, + type IncludeMap, + type IncludeRecord, + type TypeCJSON, + type TypeRecord, + legalizeName +} from "./utils"; -/* cJSON renderer */ export class CJSONRenderer extends ConvenienceRenderer { private currentFilename: string | undefined; /* Current filename */ @@ -3409,7 +3073,7 @@ export class CJSONRenderer extends ConvenienceRenderer { } else { this.emitLine( "list_add_tail(x->value, ", - // @ts-expect-error awaiting refactor + // @ts-expect-error awaiting refactor cJSON.items?.getValue, "(e), sizeof(", cJSON.items?.cType, diff --git a/packages/quicktype-core/src/language/CJSON/constants.ts b/packages/quicktype-core/src/language/CJSON/constants.ts new file mode 100644 index 000000000..cdccf419c --- /dev/null +++ b/packages/quicktype-core/src/language/CJSON/constants.ts @@ -0,0 +1,123 @@ + +/* Forbidden names for namespace */ +export const keywords = [ + /* C and C++ keywords */ + "alignas", + "alignof", + "and", + "and_eq", + "asm", + "atomic_cancel", + "atomic_commit", + "atomic_noexcept", + "auto", + "bitand", + "bitor", + "bool", + "break", + "case", + "catch", + "char", + "char16_t", + "char32_t", + "class", + "compl", + "concept", + "const", + "constexpr", + "const_cast", + "continue", + "co_await", + "co_return", + "co_yield", + "decltype", + "default", + "delete", + "do", + "double", + "dynamic_cast", + "else", + "enum", + "explicit", + "export", + "extern", + "false", + "float", + "for", + "friend", + "goto", + "if", + "import", + "inline", + "int", + "long", + "module", + "mutable", + "namespace", + "new", + "noexcept", + "not", + "not_eq", + "nullptr", + "operator", + "or", + "or_eq", + "private", + "protected", + "public", + "register", + "reinterpret_cast", + "requires", + "restrict", + "return", + "short", + "signed", + "sizeof", + "static", + "static_assert", + "static_cast", + "struct", + "switch", + "synchronized", + "template", + "this", + "thread_local", + "throw", + "true", + "try", + "typedef", + "typeid", + "typename", + "typeof", + "union", + "unsigned", + "using", + "virtual", + "void", + "volatile", + "wchar_t", + "while", + "xor", + "xor_eq", + "override", + "final", + "transaction_safe", + "transaction_safe_dynamic", + "NULL", + /* cJSON keywords */ + "Array", + "ArrayReference", + "Bool", + "DoubleArray", + "False", + "FloatArray", + "IntArray", + "Object", + "Null", + "Number", + "Raw", + "String", + "StringArray", + "StringReference", + "True" +] as const; diff --git a/packages/quicktype-core/src/language/CJSON/index.ts b/packages/quicktype-core/src/language/CJSON/index.ts new file mode 100644 index 000000000..1534721aa --- /dev/null +++ b/packages/quicktype-core/src/language/CJSON/index.ts @@ -0,0 +1,2 @@ +export { CJSONTargetLanguage, cJSONOptions } from "./language"; +export { CJSONRenderer } from "./CJSONRenderer"; diff --git a/packages/quicktype-core/src/language/CJSON/language.ts b/packages/quicktype-core/src/language/CJSON/language.ts new file mode 100644 index 000000000..c18c6806a --- /dev/null +++ b/packages/quicktype-core/src/language/CJSON/language.ts @@ -0,0 +1,170 @@ +/** + * CJSON.ts + * This file is used to generate cJSON code with quicktype + * The generated code depends of https://github.com/DaveGamble/cJSON, https://github.com/joelguittet/c-list and https://github.com/joelguittet/c-hashtable + * + * Similarly to C++ generator, it is possible to generate a single header file or multiple header files. + * To generate multiple header files, use the following option: --source-style multi-source + * + * JSON data are represented using structures, and functions in the cJSON style are created to use them. + * To parse json data from json string use the following: struct * data = cJSON_Parse(); + * To get json data from cJSON object use the following: struct * data = cJSON_GetValue(); + * To get cJSON object from json data use the following: cJSON * cjson = cJSON_Create(); + * To print json string from json data use the following: char * string = cJSON_Print(); + * To delete json data use the following: cJSON_Delete(); + * + * TODO list for future enhancements: + * - Management of Class, Union and TopLevel should be mutualized to reduce code size and to permit Union and TopLevel having recursive Array/Map + * - Types check should be added to verify unwanted inputs (for example a Number passed while a String is expected, etc) + * - Constraints should be implemented (verification of Enum values, min/max values for Numbers and min/max length for Strings, regex) + * - Support of pure Any type for example providing a callback from the application to handle these cases dynamically + * See test/languages.ts for the test cases which are not implmented/checked. + */ + +import { type RenderContext } from "../../Renderer"; +import { EnumOption, type Option, StringOption, getOptionValues } from "../../RendererOptions"; +import { type NamingStyle } from "../../support/Strings"; +import { TargetLanguage } from "../../TargetLanguage"; +import { type FixMeOptionsType, type FixMeOptionsAnyType } from "../../types"; + +import { CJSONRenderer } from "./CJSONRenderer"; + +/* Naming styles */ +const pascalValue: [string, NamingStyle] = ["pascal-case", "pascal"]; +const underscoreValue: [string, NamingStyle] = ["underscore-case", "underscore"]; +const camelValue: [string, NamingStyle] = ["camel-case", "camel"]; +const upperUnderscoreValue: [string, NamingStyle] = ["upper-underscore-case", "upper-underscore"]; +const pascalUpperAcronymsValue: [string, NamingStyle] = ["pascal-case-upper-acronyms", "pascal-upper-acronyms"]; +const camelUpperAcronymsValue: [string, NamingStyle] = ["camel-case-upper-acronyms", "camel-upper-acronyms"]; + +/* cJSON generator options */ +export const cJSONOptions = { + typeSourceStyle: new EnumOption( + "source-style", + "Source code generation type, whether to generate single or multiple source files", + [ + ["single-source", true], + ["multi-source", false] + ], + "single-source", + "secondary" + ), + typeIntegerSize: new EnumOption( + "integer-size", + "Integer code generation type (int64_t by default)", + [ + ["int8_t", "int8_t"], + ["int16_t", "int16_t"], + ["int32_t", "int32_t"], + ["int64_t", "int64_t"] + ], + "int64_t", + "secondary" + ), + hashtableSize: new StringOption( + "hashtable-size", + "Hashtable size, used when maps are created (64 by default)", + "SIZE", + "64" + ), + addTypedefAlias: new EnumOption( + "typedef-alias", + "Add typedef alias to unions, structs, and enums (no typedef by default)", + [ + ["no-typedef", false], + ["add-typedef", true] + ], + "no-typedef", + "secondary" + ), + printStyle: new EnumOption( + "print-style", + "Which cJSON print should be used (formatted by default)", + [ + ["print-formatted", false], + ["print-unformatted", true] + ], + "print-formatted", + "secondary" + ), + typeNamingStyle: new EnumOption("type-style", "Naming style for types", [ + pascalValue, + underscoreValue, + camelValue, + upperUnderscoreValue, + pascalUpperAcronymsValue, + camelUpperAcronymsValue + ]), + memberNamingStyle: new EnumOption("member-style", "Naming style for members", [ + underscoreValue, + pascalValue, + camelValue, + upperUnderscoreValue, + pascalUpperAcronymsValue, + camelUpperAcronymsValue + ]), + enumeratorNamingStyle: new EnumOption("enumerator-style", "Naming style for enumerators", [ + upperUnderscoreValue, + underscoreValue, + pascalValue, + camelValue, + pascalUpperAcronymsValue, + camelUpperAcronymsValue + ]) +}; + +/* cJSON generator target language */ +export class CJSONTargetLanguage extends TargetLanguage { + /** + * Constructor + * @param displayName: display name + * @params names: names + * @param extension: extension of files + */ + public constructor(displayName = "C (cJSON)", names: string[] = ["cjson", "cJSON"], extension = "h") { + super(displayName, names, extension); + } + + /** + * Return cJSON generator options + * @return cJSON generator options array + */ + protected getOptions(): Array> { + return [ + cJSONOptions.typeSourceStyle, + cJSONOptions.typeIntegerSize, + cJSONOptions.addTypedefAlias, + cJSONOptions.printStyle, + cJSONOptions.hashtableSize, + cJSONOptions.typeNamingStyle, + cJSONOptions.memberNamingStyle, + cJSONOptions.enumeratorNamingStyle + ]; + } + + /** + * Indicate if language support union with both number types + * @return true + */ + public get supportsUnionsWithBothNumberTypes(): boolean { + return true; + } + + /** + * Indicate if language support optional class properties + * @return true + */ + public get supportsOptionalClassProperties(): boolean { + return true; + } + + /** + * Create renderer + * @param renderContext: render context + * @param untypedOptionValues + * @return cJSON renderer + */ + protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): CJSONRenderer { + return new CJSONRenderer(this, renderContext, getOptionValues(cJSONOptions, untypedOptionValues)); + } +} diff --git a/packages/quicktype-core/src/language/CJSON/utils.ts b/packages/quicktype-core/src/language/CJSON/utils.ts new file mode 100644 index 000000000..2e74a50c7 --- /dev/null +++ b/packages/quicktype-core/src/language/CJSON/utils.ts @@ -0,0 +1,57 @@ +import { type Name } from "../../Naming"; +import { type Sourcelike } from "../../Source"; +import { isAscii, isLetterOrUnderscoreOrDigit, legalizeCharacters } from "../../support/Strings"; +import { type Type, type TypeKind } from "../../Type"; + +/* Function used to format names */ +export const legalizeName = legalizeCharacters(cp => isAscii(cp) && isLetterOrUnderscoreOrDigit(cp)); + +/* Used to build forbidden global names */ +export enum GlobalNames { + ClassMemberConstraints = 1, + ClassMemberConstraintException = 2, + ValueTooLowException = 3, + ValueTooHighException = 4, + ValueTooShortException = 5, + ValueTooLongException = 6, + InvalidPatternException = 7, + CheckConstraint = 8 +} + +/* To be able to support circles in multiple files - e.g. class#A using class#B using class#A (obviously not directly) we can forward declare them */ +export enum IncludeKind { + ForwardDeclare = "ForwardDeclare", + Include = "Include" +} + +/* Used to map includes */ +export interface IncludeRecord { + kind: IncludeKind | undefined /* How to include that */; + typeKind: TypeKind | undefined /* What exactly to include */; +} + +/* Used to map includes */ +export interface TypeRecord { + forceInclude: boolean; + level: number; + name: Name; + type: Type; + variant: boolean; +} + +/* Map each and every unique type to a include kind, e.g. how to include the given type */ +export type IncludeMap = Map; + +/* cJSON type */ +export interface TypeCJSON { + addToObject: Sourcelike /* cJSON add to object function */; + cType: Sourcelike /* C type */; + cjsonType: string /* cJSON type */; + createObject: Sourcelike /* cJSON create object function */; + deleteType: Sourcelike /* cJSON delete function */; + getValue: Sourcelike /* cJSON get value function */; + isNullable: boolean /* True if the field is nullable */; + isType: Sourcelike /* cJSON check type function */; + items: TypeCJSON | undefined /* Sub-items, used for arrays and map */; + optionalQualifier: string /* C optional qualifier, empty string if not defined */; +} diff --git a/packages/quicktype-core/src/language/CPlusPlus.ts b/packages/quicktype-core/src/language/CPlusPlus/CPlusPlusRenderer.ts similarity index 89% rename from packages/quicktype-core/src/language/CPlusPlus.ts rename to packages/quicktype-core/src/language/CPlusPlus/CPlusPlusRenderer.ts index 910f714f9..4b70339f2 100644 --- a/packages/quicktype-core/src/language/CPlusPlus.ts +++ b/packages/quicktype-core/src/language/CPlusPlus/CPlusPlusRenderer.ts @@ -8,473 +8,47 @@ import { withDefault } from "collection-utils"; -import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../Annotation"; -import { getAccessorName } from "../attributes/AccessorNames"; +import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../../Annotation"; +import { getAccessorName } from "../../attributes/AccessorNames"; +import { enumCaseValues } from "../../attributes/EnumValues"; +import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../../ConvenienceRenderer"; +import { type Declaration } from "../../DeclarationIR"; +import { DependencyName, type Name, type NameStyle, type Namer, funPrefixNamer } from "../../Naming"; +import { type RenderContext } from "../../Renderer"; +import { type OptionValues } from "../../RendererOptions"; +import { type Sourcelike, maybeAnnotated } from "../../Source"; +import { type NamingStyle, makeNameStyle, stringEscape } from "../../support/Strings"; +import { assert, assertNever, defined, numberEnumValues, panic } from "../../support/Support"; +import { type TargetLanguage } from "../../TargetLanguage"; +import { ArrayType, type ClassProperty, ClassType, EnumType, MapType, type Type, UnionType } from "../../Type"; import { - type MinMaxConstraint, - minMaxLengthForType, - minMaxValueForType, - patternForType -} from "../attributes/Constraints"; -import { enumCaseValues } from "../attributes/EnumValues"; -import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../ConvenienceRenderer"; -import { type Declaration } from "../DeclarationIR"; -import { DependencyName, type Name, type NameStyle, type Namer, funPrefixNamer } from "../Naming"; -import { type RenderContext } from "../Renderer"; + directlyReachableTypes, + isNamedType, + matchType, + nullableFromUnion, + removeNullFromUnion +} from "../../TypeUtils"; + +import { keywords } from "./constants"; +import { type cPlusPlusOptions } from "./language"; import { - BooleanOption, - EnumOption, - type Option, - type OptionValues, - StringOption, - getOptionValues -} from "../RendererOptions"; -import { type Sourcelike, maybeAnnotated } from "../Source"; -import { - type NamingStyle, - isAscii, - isLetterOrUnderscoreOrDigit, - legalizeCharacters, - makeNameStyle, - stringEscape -} from "../support/Strings"; -import { assert, assertNever, defined, numberEnumValues, panic } from "../support/Support"; -import { TargetLanguage } from "../TargetLanguage"; -import { - ArrayType, - type ClassProperty, - ClassType, - EnumType, - MapType, - type Type, - type TypeKind, - UnionType -} from "../Type"; -import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../types"; -import { directlyReachableTypes, isNamedType, matchType, nullableFromUnion, removeNullFromUnion } from "../TypeUtils"; - -const pascalValue: [string, NamingStyle] = ["pascal-case", "pascal"]; -const underscoreValue: [string, NamingStyle] = ["underscore-case", "underscore"]; -const camelValue: [string, NamingStyle] = ["camel-case", "camel"]; -const upperUnderscoreValue: [string, NamingStyle] = ["upper-underscore-case", "upper-underscore"]; -const pascalUpperAcronymsValue: [string, NamingStyle] = ["pascal-case-upper-acronyms", "pascal-upper-acronyms"]; -const camelUpperAcronymsValue: [string, NamingStyle] = ["camel-case-upper-acronyms", "camel-upper-acronyms"]; - -export const cPlusPlusOptions = { - typeSourceStyle: new EnumOption( - "source-style", - "Source code generation type, whether to generate single or multiple source files", - [ - ["single-source", true], - ["multi-source", false] - ], - "single-source", - "secondary" - ), - includeLocation: new EnumOption( - "include-location", - "Whether json.hpp is to be located globally or locally", - [ - ["local-include", true], - ["global-include", false] - ], - "local-include", - "secondary" - ), - codeFormat: new EnumOption( - "code-format", - "Generate classes with getters/setters, instead of structs", - [ - ["with-struct", false], - ["with-getter-setter", true] - ], - "with-getter-setter" - ), - wstring: new EnumOption( - "wstring", - "Store strings using Utf-16 std::wstring, rather than Utf-8 std::string", - [ - ["use-string", false], - ["use-wstring", true] - ], - "use-string" - ), - westConst: new EnumOption( - "const-style", - "Put const to the left/west (const T) or right/east (T const)", - [ - ["west-const", true], - ["east-const", false] - ], - "west-const" - ), - justTypes: new BooleanOption("just-types", "Plain types only", false), - namespace: new StringOption("namespace", "Name of the generated namespace(s)", "NAME", "quicktype"), - enumType: new StringOption("enum-type", "Type of enum class", "NAME", "int", "secondary"), - typeNamingStyle: new EnumOption("type-style", "Naming style for types", [ - pascalValue, - underscoreValue, - camelValue, - upperUnderscoreValue, - pascalUpperAcronymsValue, - camelUpperAcronymsValue - ]), - memberNamingStyle: new EnumOption("member-style", "Naming style for members", [ - underscoreValue, - pascalValue, - camelValue, - upperUnderscoreValue, - pascalUpperAcronymsValue, - camelUpperAcronymsValue - ]), - enumeratorNamingStyle: new EnumOption("enumerator-style", "Naming style for enumerators", [ - upperUnderscoreValue, - underscoreValue, - pascalValue, - camelValue, - pascalUpperAcronymsValue, - camelUpperAcronymsValue - ]), - boost: new BooleanOption("boost", "Require a dependency on boost. Without boost, C++17 is required", true), - hideNullOptional: new BooleanOption("hide-null-optional", "Hide null value for optional field", false) -}; - -export class CPlusPlusTargetLanguage extends TargetLanguage { - public constructor(displayName = "C++", names: string[] = ["c++", "cpp", "cplusplus"], extension = "cpp") { - super(displayName, names, extension); - } - - protected getOptions(): Array> { - return [ - cPlusPlusOptions.justTypes, - cPlusPlusOptions.namespace, - cPlusPlusOptions.codeFormat, - cPlusPlusOptions.wstring, - cPlusPlusOptions.westConst, - cPlusPlusOptions.typeSourceStyle, - cPlusPlusOptions.includeLocation, - cPlusPlusOptions.typeNamingStyle, - cPlusPlusOptions.memberNamingStyle, - cPlusPlusOptions.enumeratorNamingStyle, - cPlusPlusOptions.enumType, - cPlusPlusOptions.boost, - cPlusPlusOptions.hideNullOptional - ]; - } - - public get supportsUnionsWithBothNumberTypes(): boolean { - return true; - } - - public get supportsOptionalClassProperties(): boolean { - return true; - } - - protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): CPlusPlusRenderer { - return new CPlusPlusRenderer(this, renderContext, getOptionValues(cPlusPlusOptions, untypedOptionValues)); - } -} - -function constraintsForType(t: Type): - | { - minMax?: MinMaxConstraint; - minMaxLength?: MinMaxConstraint; - pattern?: string; - } - | undefined { - const minMax = minMaxValueForType(t); - const minMaxLength = minMaxLengthForType(t); - const pattern = patternForType(t); - if (minMax === undefined && minMaxLength === undefined && pattern === undefined) return undefined; - return { minMax, minMaxLength, pattern }; -} - -const legalizeName = legalizeCharacters(cp => isAscii(cp) && isLetterOrUnderscoreOrDigit(cp)); - -const keywords = [ - "alignas", - "alignof", - "and", - "and_eq", - "asm", - "atomic_cancel", - "atomic_commit", - "atomic_noexcept", - "auto", - "bitand", - "bitor", - "bool", - "break", - "case", - "catch", - "char", - "char16_t", - "char32_t", - "class", - "compl", - "concept", - "const", - "constexpr", - "const_cast", - "continue", - "co_await", - "co_return", - "co_yield", - "decltype", - "default", - "delete", - "do", - "double", - "dynamic_cast", - "else", - "enum", - "explicit", - "export", - "extern", - "false", - "float", - "for", - "friend", - "goto", - "if", - "import", - "inline", - "int", - "long", - "module", - "mutable", - "namespace", - "new", - "noexcept", - "not", - "not_eq", - "nullptr", - "operator", - "or", - "or_eq", - "private", - "protected", - "public", - "register", - "reinterpret_cast", - "requires", - "return", - "short", - "signed", - "sizeof", - "static", - "static_assert", - "static_cast", - "struct", - "switch", - "synchronized", - "template", - "this", - "thread_local", - "throw", - "true", - "try", - "typedef", - "typeid", - "typename", - "union", - "unsigned", - "using", - "virtual", - "void", - "volatile", - "wchar_t", - "while", - "xor", - "xor_eq", - "override", - "final", - "transaction_safe", - "transaction_safe_dynamic", - "NULL" -]; - -/// Type to use as an optional if cycle breaking is required -const optionalAsSharedType = "std::shared_ptr"; -/// Factory to use when creating an optional if cycle breaking is required -const optionalFactoryAsSharedType = "std::make_shared"; - -/** - * To be able to support circles in multiple files - - * e.g. class#A using class#B using class#A (obviously not directly, - * but in vector or in variant) we can forward declare them; - */ -export enum IncludeKind { - ForwardDeclare = "ForwardDeclare", - Include = "Include" -} - -// FIXME: make these string enums eventually -export enum GlobalNames { - ClassMemberConstraints = 1, - ClassMemberConstraintException = 2, - ValueTooLowException = 3, - ValueTooHighException = 4, - ValueTooShortException = 5, - ValueTooLongException = 6, - InvalidPatternException = 7, - CheckConstraint = 8 -} - -// FIXME: make these string enums eventually -export enum MemberNames { - MinIntValue = 1, - GetMinIntValue = 2, - SetMinIntValue = 3, - MaxIntValue = 4, - GetMaxIntValue = 5, - SetMaxIntValue = 6, - MinDoubleValue = 7, - GetMinDoubleValue = 8, - SetMinDoubleValue = 9, - MaxDoubleValue = 10, - GetMaxDoubleValue = 11, - SetMaxDoubleValue = 12, - MinLength = 13, - GetMinLength = 14, - SetMinLength = 15, - MaxLength = 16, - GetMaxLength = 17, - SetMaxLength = 18, - Pattern = 19, - GetPattern = 20, - SetPattern = 21 -} - -interface ConstraintMember { - cppConstType?: string; - cppType: string; - getter: MemberNames; - name: MemberNames; - setter: MemberNames; -} - -export interface IncludeRecord { - kind: IncludeKind | undefined /** How to include that */; - typeKind: TypeKind | undefined /** What exactly to include */; -} - -export interface TypeRecord { - forceInclude: boolean; - level: number; - name: Name; - type: Type; - variant: boolean; -} - -/** - * We map each and every unique type to a include kind, e.g. how - * to include the given type - */ -export type IncludeMap = Map; - -export interface TypeContext { - inJsonNamespace: boolean; - needsForwardIndirection: boolean; - needsOptionalIndirection: boolean; -} - -interface StringType { - createStringLiteral: (inner: Sourcelike) => Sourcelike; - emitHelperFunctions: () => void; - getConstType: () => string; - getRegex: () => string; - getSMatch: () => string; - getType: () => string; - wrapEncodingChange: ( - qualifier: Sourcelike[], - fromType: Sourcelike, - toType: Sourcelike, - inner: Sourcelike - ) => Sourcelike; - wrapToString: (inner: Sourcelike) => Sourcelike; -} - -function addQualifier(qualifier: Sourcelike, qualified: Sourcelike[]): Sourcelike[] { - if (qualified.length === 0) { - return []; - } - - return [qualifier, qualified]; -} - -class WrappingCode { - public constructor( - private readonly start: Sourcelike[], - private readonly end: Sourcelike[] - ) {} - - public wrap(qualifier: Sourcelike, inner: Sourcelike): Sourcelike { - return [addQualifier(qualifier, this.start), inner, this.end]; - } -} - -class BaseString { - public _stringType: string; - - public _constStringType: string; - - public _smatch: string; - - public _regex: string; - - public _stringLiteralPrefix: string; - - public _toString: WrappingCode; - - public _encodingClass: Sourcelike; - - public _encodingFunction: Sourcelike; - - public constructor( - stringType: string, - constStringType: string, - smatch: string, - regex: string, - stringLiteralPrefix: string, - toString: WrappingCode, - encodingClass: string, - encodingFunction: string - ) { - this._stringType = stringType; - this._constStringType = constStringType; - this._smatch = smatch; - this._regex = regex; - this._stringLiteralPrefix = stringLiteralPrefix; - this._toString = toString; - this._encodingClass = encodingClass; - this._encodingFunction = encodingFunction; - } - - public getType(): string { - return this._stringType; - } - - public getConstType(): string { - return this._constStringType; - } - - public getSMatch(): string { - return this._smatch; - } - - public getRegex(): string { - return this._regex; - } - - public createStringLiteral(inner: Sourcelike): Sourcelike { - return [this._stringLiteralPrefix, '"', inner, '"']; - } - - public wrapToString(inner: Sourcelike): Sourcelike { - return this._toString.wrap([], inner); - } -} + BaseString, + type ConstraintMember, + GlobalNames, + IncludeKind, + type IncludeMap, + type IncludeRecord, + MemberNames, + type StringType, + type TypeContext, + type TypeRecord, + WrappingCode, + addQualifier, + constraintsForType, + legalizeName, + optionalAsSharedType, + optionalFactoryAsSharedType +} from "./utils"; export class CPlusPlusRenderer extends ConvenienceRenderer { /** diff --git a/packages/quicktype-core/src/language/CPlusPlus/constants.ts b/packages/quicktype-core/src/language/CPlusPlus/constants.ts new file mode 100644 index 000000000..4ed53bd81 --- /dev/null +++ b/packages/quicktype-core/src/language/CPlusPlus/constants.ts @@ -0,0 +1,103 @@ + +export const keywords = [ + "alignas", + "alignof", + "and", + "and_eq", + "asm", + "atomic_cancel", + "atomic_commit", + "atomic_noexcept", + "auto", + "bitand", + "bitor", + "bool", + "break", + "case", + "catch", + "char", + "char16_t", + "char32_t", + "class", + "compl", + "concept", + "const", + "constexpr", + "const_cast", + "continue", + "co_await", + "co_return", + "co_yield", + "decltype", + "default", + "delete", + "do", + "double", + "dynamic_cast", + "else", + "enum", + "explicit", + "export", + "extern", + "false", + "float", + "for", + "friend", + "goto", + "if", + "import", + "inline", + "int", + "long", + "module", + "mutable", + "namespace", + "new", + "noexcept", + "not", + "not_eq", + "nullptr", + "operator", + "or", + "or_eq", + "private", + "protected", + "public", + "register", + "reinterpret_cast", + "requires", + "return", + "short", + "signed", + "sizeof", + "static", + "static_assert", + "static_cast", + "struct", + "switch", + "synchronized", + "template", + "this", + "thread_local", + "throw", + "true", + "try", + "typedef", + "typeid", + "typename", + "union", + "unsigned", + "using", + "virtual", + "void", + "volatile", + "wchar_t", + "while", + "xor", + "xor_eq", + "override", + "final", + "transaction_safe", + "transaction_safe_dynamic", + "NULL" +] as const; diff --git a/packages/quicktype-core/src/language/CPlusPlus/index.ts b/packages/quicktype-core/src/language/CPlusPlus/index.ts new file mode 100644 index 000000000..8ee7d4d66 --- /dev/null +++ b/packages/quicktype-core/src/language/CPlusPlus/index.ts @@ -0,0 +1,2 @@ +export { CPlusPlusTargetLanguage, cPlusPlusOptions } from "./language"; +export { CPlusPlusRenderer } from "./CPlusPlusRenderer"; diff --git a/packages/quicktype-core/src/language/CPlusPlus/language.ts b/packages/quicktype-core/src/language/CPlusPlus/language.ts new file mode 100644 index 000000000..292d21a11 --- /dev/null +++ b/packages/quicktype-core/src/language/CPlusPlus/language.ts @@ -0,0 +1,129 @@ +import { type RenderContext } from "../../Renderer"; +import { BooleanOption, EnumOption, type Option, StringOption, getOptionValues } from "../../RendererOptions"; +import { type NamingStyle } from "../../support/Strings"; +import { TargetLanguage } from "../../TargetLanguage"; +import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../../types"; + +import { CPlusPlusRenderer } from "./CPlusPlusRenderer"; + +const pascalValue: [string, NamingStyle] = ["pascal-case", "pascal"]; +const underscoreValue: [string, NamingStyle] = ["underscore-case", "underscore"]; +const camelValue: [string, NamingStyle] = ["camel-case", "camel"]; +const upperUnderscoreValue: [string, NamingStyle] = ["upper-underscore-case", "upper-underscore"]; +const pascalUpperAcronymsValue: [string, NamingStyle] = ["pascal-case-upper-acronyms", "pascal-upper-acronyms"]; +const camelUpperAcronymsValue: [string, NamingStyle] = ["camel-case-upper-acronyms", "camel-upper-acronyms"]; + +export const cPlusPlusOptions = { + typeSourceStyle: new EnumOption( + "source-style", + "Source code generation type, whether to generate single or multiple source files", + [ + ["single-source", true], + ["multi-source", false] + ], + "single-source", + "secondary" + ), + includeLocation: new EnumOption( + "include-location", + "Whether json.hpp is to be located globally or locally", + [ + ["local-include", true], + ["global-include", false] + ], + "local-include", + "secondary" + ), + codeFormat: new EnumOption( + "code-format", + "Generate classes with getters/setters, instead of structs", + [ + ["with-struct", false], + ["with-getter-setter", true] + ], + "with-getter-setter" + ), + wstring: new EnumOption( + "wstring", + "Store strings using Utf-16 std::wstring, rather than Utf-8 std::string", + [ + ["use-string", false], + ["use-wstring", true] + ], + "use-string" + ), + westConst: new EnumOption( + "const-style", + "Put const to the left/west (const T) or right/east (T const)", + [ + ["west-const", true], + ["east-const", false] + ], + "west-const" + ), + justTypes: new BooleanOption("just-types", "Plain types only", false), + namespace: new StringOption("namespace", "Name of the generated namespace(s)", "NAME", "quicktype"), + enumType: new StringOption("enum-type", "Type of enum class", "NAME", "int", "secondary"), + typeNamingStyle: new EnumOption("type-style", "Naming style for types", [ + pascalValue, + underscoreValue, + camelValue, + upperUnderscoreValue, + pascalUpperAcronymsValue, + camelUpperAcronymsValue + ]), + memberNamingStyle: new EnumOption("member-style", "Naming style for members", [ + underscoreValue, + pascalValue, + camelValue, + upperUnderscoreValue, + pascalUpperAcronymsValue, + camelUpperAcronymsValue + ]), + enumeratorNamingStyle: new EnumOption("enumerator-style", "Naming style for enumerators", [ + upperUnderscoreValue, + underscoreValue, + pascalValue, + camelValue, + pascalUpperAcronymsValue, + camelUpperAcronymsValue + ]), + boost: new BooleanOption("boost", "Require a dependency on boost. Without boost, C++17 is required", true), + hideNullOptional: new BooleanOption("hide-null-optional", "Hide null value for optional field", false) +}; + +export class CPlusPlusTargetLanguage extends TargetLanguage { + public constructor(displayName = "C++", names: string[] = ["c++", "cpp", "cplusplus"], extension = "cpp") { + super(displayName, names, extension); + } + + protected getOptions(): Array> { + return [ + cPlusPlusOptions.justTypes, + cPlusPlusOptions.namespace, + cPlusPlusOptions.codeFormat, + cPlusPlusOptions.wstring, + cPlusPlusOptions.westConst, + cPlusPlusOptions.typeSourceStyle, + cPlusPlusOptions.includeLocation, + cPlusPlusOptions.typeNamingStyle, + cPlusPlusOptions.memberNamingStyle, + cPlusPlusOptions.enumeratorNamingStyle, + cPlusPlusOptions.enumType, + cPlusPlusOptions.boost, + cPlusPlusOptions.hideNullOptional + ]; + } + + public get supportsUnionsWithBothNumberTypes(): boolean { + return true; + } + + public get supportsOptionalClassProperties(): boolean { + return true; + } + + protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): CPlusPlusRenderer { + return new CPlusPlusRenderer(this, renderContext, getOptionValues(cPlusPlusOptions, untypedOptionValues)); + } +} diff --git a/packages/quicktype-core/src/language/CPlusPlus/utils.ts b/packages/quicktype-core/src/language/CPlusPlus/utils.ts new file mode 100644 index 000000000..93c6e75de --- /dev/null +++ b/packages/quicktype-core/src/language/CPlusPlus/utils.ts @@ -0,0 +1,208 @@ +import { + type MinMaxConstraint, + minMaxLengthForType, + minMaxValueForType, + patternForType +} from "../../attributes/Constraints"; +import { type Name } from "../../Naming"; +import { type Sourcelike } from "../../Source"; +import { isAscii, isLetterOrUnderscoreOrDigit, legalizeCharacters } from "../../support/Strings"; +import { type Type, type TypeKind } from "../../Type"; + +export function constraintsForType(t: Type): + | { + minMax?: MinMaxConstraint; + minMaxLength?: MinMaxConstraint; + pattern?: string; + } + | undefined { + const minMax = minMaxValueForType(t); + const minMaxLength = minMaxLengthForType(t); + const pattern = patternForType(t); + if (minMax === undefined && minMaxLength === undefined && pattern === undefined) return undefined; + return { minMax, minMaxLength, pattern }; +} + +export const legalizeName = legalizeCharacters(cp => isAscii(cp) && isLetterOrUnderscoreOrDigit(cp)); + +/// Type to use as an optional if cycle breaking is required +export const optionalAsSharedType = "std::shared_ptr"; +/// Factory to use when creating an optional if cycle breaking is required +export const optionalFactoryAsSharedType = "std::make_shared"; + +/** + * To be able to support circles in multiple files - + * e.g. class#A using class#B using class#A (obviously not directly, + * but in vector or in variant) we can forward declare them; + */ +export enum IncludeKind { + ForwardDeclare = "ForwardDeclare", + Include = "Include" +} + +// FIXME: make these string enums eventually +export enum GlobalNames { + ClassMemberConstraints = 1, + ClassMemberConstraintException = 2, + ValueTooLowException = 3, + ValueTooHighException = 4, + ValueTooShortException = 5, + ValueTooLongException = 6, + InvalidPatternException = 7, + CheckConstraint = 8 +} + +// FIXME: make these string enums eventually +export enum MemberNames { + MinIntValue = 1, + GetMinIntValue = 2, + SetMinIntValue = 3, + MaxIntValue = 4, + GetMaxIntValue = 5, + SetMaxIntValue = 6, + MinDoubleValue = 7, + GetMinDoubleValue = 8, + SetMinDoubleValue = 9, + MaxDoubleValue = 10, + GetMaxDoubleValue = 11, + SetMaxDoubleValue = 12, + MinLength = 13, + GetMinLength = 14, + SetMinLength = 15, + MaxLength = 16, + GetMaxLength = 17, + SetMaxLength = 18, + Pattern = 19, + GetPattern = 20, + SetPattern = 21 +} + +export interface ConstraintMember { + cppConstType?: string; + cppType: string; + getter: MemberNames; + name: MemberNames; + setter: MemberNames; +} + +export interface IncludeRecord { + kind: IncludeKind | undefined /** How to include that */; + typeKind: TypeKind | undefined /** What exactly to include */; +} + +export interface TypeRecord { + forceInclude: boolean; + level: number; + name: Name; + type: Type; + variant: boolean; +} + +/** + * We map each and every unique type to a include kind, e.g. how + * to include the given type + */ +export type IncludeMap = Map; + +export interface TypeContext { + inJsonNamespace: boolean; + needsForwardIndirection: boolean; + needsOptionalIndirection: boolean; +} + +export interface StringType { + createStringLiteral: (inner: Sourcelike) => Sourcelike; + emitHelperFunctions: () => void; + getConstType: () => string; + getRegex: () => string; + getSMatch: () => string; + getType: () => string; + wrapEncodingChange: ( + qualifier: Sourcelike[], + fromType: Sourcelike, + toType: Sourcelike, + inner: Sourcelike + ) => Sourcelike; + wrapToString: (inner: Sourcelike) => Sourcelike; +} + +export function addQualifier(qualifier: Sourcelike, qualified: Sourcelike[]): Sourcelike[] { + if (qualified.length === 0) { + return []; + } + + return [qualifier, qualified]; +} + +export class WrappingCode { + public constructor( + private readonly start: Sourcelike[], + private readonly end: Sourcelike[] + ) {} + + public wrap(qualifier: Sourcelike, inner: Sourcelike): Sourcelike { + return [addQualifier(qualifier, this.start), inner, this.end]; + } +} + +export class BaseString { + public _stringType: string; + + public _constStringType: string; + + public _smatch: string; + + public _regex: string; + + public _stringLiteralPrefix: string; + + public _toString: WrappingCode; + + public _encodingClass: Sourcelike; + + public _encodingFunction: Sourcelike; + + public constructor( + stringType: string, + constStringType: string, + smatch: string, + regex: string, + stringLiteralPrefix: string, + toString: WrappingCode, + encodingClass: string, + encodingFunction: string + ) { + this._stringType = stringType; + this._constStringType = constStringType; + this._smatch = smatch; + this._regex = regex; + this._stringLiteralPrefix = stringLiteralPrefix; + this._toString = toString; + this._encodingClass = encodingClass; + this._encodingFunction = encodingFunction; + } + + public getType(): string { + return this._stringType; + } + + public getConstType(): string { + return this._constStringType; + } + + public getSMatch(): string { + return this._smatch; + } + + public getRegex(): string { + return this._regex; + } + + public createStringLiteral(inner: Sourcelike): Sourcelike { + return [this._stringLiteralPrefix, '"', inner, '"']; + } + + public wrapToString(inner: Sourcelike): Sourcelike { + return this._toString.wrap([], inner); + } +} diff --git a/packages/quicktype-core/src/language/CSharp.ts b/packages/quicktype-core/src/language/CSharp.ts deleted file mode 100644 index 331345d66..000000000 --- a/packages/quicktype-core/src/language/CSharp.ts +++ /dev/null @@ -1,2498 +0,0 @@ -import { arrayIntercalate } from "collection-utils"; -import unicode from "unicode-properties"; - -import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../Annotation"; -import { minMaxLengthForType, minMaxValueForType } from "../attributes/Constraints"; -import { ConvenienceRenderer, type ForbiddenWordsInfo, inferredNameOrder } from "../ConvenienceRenderer"; -import { DependencyName, type Name, type Namer, SimpleName, funPrefixNamer } from "../Naming"; -import { type RenderContext } from "../Renderer"; -import { - BooleanOption, - EnumOption, - type Option, - type OptionValues, - StringOption, - getOptionValues -} from "../RendererOptions"; -import { type Sourcelike, maybeAnnotated, modifySource } from "../Source"; -import { - type WordInName, - camelCase, - combineWords, - firstUpperWordStyle, - splitIntoWords, - utf16LegalizeCharacters, - utf16StringEscape -} from "../support/Strings"; -import { assert, assertNever, defined, panic } from "../support/Support"; -import { TargetLanguage } from "../TargetLanguage"; -import { - ArrayDecodingTransformer, - ArrayEncodingTransformer, - ChoiceTransformer, - DecodingChoiceTransformer, - DecodingTransformer, - EncodingTransformer, - MinMaxLengthCheckTransformer, - MinMaxValueTransformer, - ParseStringTransformer, - StringMatchTransformer, - StringProducerTransformer, - StringifyTransformer, - type Transformation, - type Transformer, - UnionInstantiationTransformer, - UnionMemberMatchTransformer, - followTargetType, - transformationForType -} from "../Transformers"; -import { - ArrayType, - type ClassProperty, - ClassType, - EnumType, - type PrimitiveStringTypeKind, - type PrimitiveType, - type TransformedStringTypeKind, - type Type, - UnionType -} from "../Type"; -import { type StringTypeMapping } from "../TypeBuilder"; -import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../types"; -import { directlyReachableSingleNamedType, matchType, nullableFromUnion, removeNullFromUnion } from "../TypeUtils"; - -export enum Framework { - Newtonsoft = "Newtonsoft", - SystemTextJson = "SystemTextJson" -} - -export type Version = 5 | 6; -export interface OutputFeatures { - attributes: boolean; - helpers: boolean; -} - -export enum AccessModifier { - None = "None", - Public = "Public", - Internal = "Internal" -} - -export type CSharpTypeForAny = "object" | "dynamic"; - -function noFollow(t: Type): Type { - return t; -} - -function needTransformerForType(t: Type): "automatic" | "manual" | "nullable" | "none" { - if (t instanceof UnionType) { - const maybeNullable = nullableFromUnion(t); - if (maybeNullable === null) return "automatic"; - if (needTransformerForType(maybeNullable) === "manual") return "nullable"; - return "none"; - } - - if (t instanceof ArrayType) { - const itemsNeed = needTransformerForType(t.items); - if (itemsNeed === "manual" || itemsNeed === "nullable") return "automatic"; - return "none"; - } - - if (t instanceof EnumType) return "automatic"; - if (t.kind === "double") return minMaxValueForType(t) !== undefined ? "manual" : "none"; - if (t.kind === "integer-string" || t.kind === "bool-string") return "manual"; - if (t.kind === "string") { - return minMaxLengthForType(t) !== undefined ? "manual" : "none"; - } - - return "none"; -} - -function alwaysApplyTransformation(xf: Transformation): boolean { - const t = xf.targetType; - if (t instanceof EnumType) return true; - if (t instanceof UnionType) return nullableFromUnion(t) === null; - return false; -} - -/** - * The C# type for a given transformed string type. - */ -function csTypeForTransformedStringType(t: PrimitiveType): Sourcelike { - switch (t.kind) { - case "date-time": - return "DateTimeOffset"; - case "uuid": - return "Guid"; - case "uri": - return "Uri"; - default: - return panic(`Transformed string type ${t.kind} not supported`); - } -} - -export const cSharpOptions = { - framework: new EnumOption( - "framework", - "Serialization framework", - [ - ["NewtonSoft", Framework.Newtonsoft], - ["SystemTextJson", Framework.SystemTextJson] - ], - "NewtonSoft" - ), - useList: new EnumOption("array-type", "Use T[] or List", [ - ["array", false], - ["list", true] - ]), - dense: new EnumOption( - "density", - "Property density", - [ - ["normal", false], - ["dense", true] - ], - "normal", - "secondary" - ), - // FIXME: Do this via a configurable named eventually. - namespace: new StringOption("namespace", "Generated namespace", "NAME", "QuickType"), - version: new EnumOption( - "csharp-version", - "C# version", - [ - ["5", 5], - ["6", 6] - ], - "6", - "secondary" - ), - virtual: new BooleanOption("virtual", "Generate virtual properties", false), - typeForAny: new EnumOption( - "any-type", - 'Type to use for "any"', - [ - ["object", "object"], - ["dynamic", "dynamic"] - ], - "object", - "secondary" - ), - useDecimal: new EnumOption( - "number-type", - "Type to use for numbers", - [ - ["double", false], - ["decimal", true] - ], - "double", - "secondary" - ), - features: new EnumOption("features", "Output features", [ - ["complete", { namespaces: true, helpers: true, attributes: true }], - ["attributes-only", { namespaces: true, helpers: false, attributes: true }], - ["just-types-and-namespace", { namespaces: true, helpers: false, attributes: false }], - ["just-types", { namespaces: true, helpers: false, attributes: false }] - ]), - baseclass: new EnumOption( - "base-class", - "Base class", - [ - ["EntityData", "EntityData"], - ["Object", undefined] - ], - "Object", - "secondary" - ), - checkRequired: new BooleanOption("check-required", "Fail if required properties are missing", false), - keepPropertyName: new BooleanOption("keep-property-name", "Keep original field name generate", false) -}; - -export class CSharpTargetLanguage extends TargetLanguage { - public constructor() { - super("C#", ["cs", "csharp"], "cs"); - } - - protected getOptions(): Array> { - return [ - cSharpOptions.framework, - cSharpOptions.namespace, - cSharpOptions.version, - cSharpOptions.dense, - cSharpOptions.useList, - cSharpOptions.useDecimal, - cSharpOptions.typeForAny, - cSharpOptions.virtual, - cSharpOptions.features, - cSharpOptions.baseclass, - cSharpOptions.checkRequired, - cSharpOptions.keepPropertyName - ]; - } - - public get stringTypeMapping(): StringTypeMapping { - const mapping: Map = new Map(); - mapping.set("date", "date-time"); - mapping.set("time", "date-time"); - mapping.set("date-time", "date-time"); - mapping.set("uuid", "uuid"); - mapping.set("uri", "uri"); - mapping.set("integer-string", "integer-string"); - mapping.set("bool-string", "bool-string"); - return mapping; - } - - public get supportsUnionsWithBothNumberTypes(): boolean { - return true; - } - - public get supportsOptionalClassProperties(): boolean { - return true; - } - - public needsTransformerForType(t: Type): boolean { - const need = needTransformerForType(t); - return need !== "none" && need !== "nullable"; - } - - protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): ConvenienceRenderer { - const options = getOptionValues(cSharpOptions, untypedOptionValues); - - switch (options.framework) { - case Framework.Newtonsoft: - return new NewtonsoftCSharpRenderer( - this, - renderContext, - getOptionValues(newtonsoftCSharpOptions, untypedOptionValues) - ); - case Framework.SystemTextJson: - return new SystemTextJsonCSharpRenderer( - this, - renderContext, - getOptionValues(systemTextJsonCSharpOptions, untypedOptionValues) - ); - default: - return assertNever(options.framework); - } - } -} - -const namingFunction = funPrefixNamer("namer", csNameStyle); -const namingFunctionKeep = funPrefixNamer("namerKeep", csNameStyleKeep); - -// FIXME: Make a Named? -const denseJsonPropertyName = "J"; -const denseRequiredEnumName = "R"; -const denseNullValueHandlingEnumName = "N"; - -function isStartCharacter(utf16Unit: number): boolean { - if (unicode.isAlphabetic(utf16Unit)) { - return true; - } - - return utf16Unit === 0x5f; // underscore -} - -function isPartCharacter(utf16Unit: number): boolean { - const category: string = unicode.getCategory(utf16Unit); - if (["Nd", "Pc", "Mn", "Mc"].includes(category)) { - return true; - } - - return isStartCharacter(utf16Unit); -} - -const legalizeName = utf16LegalizeCharacters(isPartCharacter); - -function csNameStyle(original: string): string { - const words = splitIntoWords(original); - return combineWords( - words, - legalizeName, - firstUpperWordStyle, - firstUpperWordStyle, - firstUpperWordStyle, - firstUpperWordStyle, - "", - isStartCharacter - ); -} - -function csNameStyleKeep(original: string): string { - const keywords = [ - "abstract", - "as", - "base", - "bool", - "break", - "byte", - "case", - "catch", - "char", - "checked", - "class", - "const", - "continue", - "decimal", - "default", - "delegate", - "do", - "double", - "else", - "enum", - "event", - "explicit", - "extern", - "false", - "finally", - "fixed", - "float", - "for", - "foreach", - "goto", - "if", - "implicit", - "in", - "int", - "interface", - "internal", - "is", - "lock", - "long", - "namespace", - "new", - "null", - "object", - "operator", - "out", - "override", - "params", - "private", - "protected", - "public", - "readonly", - "ref", - "return", - "sbyte", - "sealed", - "short", - "sizeof", - "stackalloc", - "static", - "string", - "struct", - "switch", - "this", - "throw", - "true", - "try", - "typeof", - "uint", - "ulong", - "unchecked", - "unsafe", - "ushort", - "using", - "virtual", - "void", - "volatile", - "while" - ]; - - const words: WordInName[] = [ - { - word: original, - isAcronym: false - } - ]; - - const result = combineWords( - words, - legalizeName, - x => x, - x => x, - x => x, - x => x, - "", - isStartCharacter - ); - - return keywords.includes(result) ? "@" + result : result; -} - -function isValueType(t: Type): boolean { - if (t instanceof UnionType) { - return nullableFromUnion(t) === null; - } - - return ["integer", "double", "bool", "enum", "date-time", "uuid"].includes(t.kind); -} - -export class CSharpRenderer extends ConvenienceRenderer { - public constructor( - targetLanguage: TargetLanguage, - renderContext: RenderContext, - private readonly _csOptions: OptionValues - ) { - super(targetLanguage, renderContext); - } - - protected forbiddenNamesForGlobalNamespace(): string[] { - return ["QuickType", "Type", "System", "Console", "Exception", "DateTimeOffset", "Guid", "Uri"]; - } - - protected forbiddenForObjectProperties(_: ClassType, classNamed: Name): ForbiddenWordsInfo { - return { - names: [ - classNamed, - "ToString", - "GetHashCode", - "Finalize", - "Equals", - "GetType", - "MemberwiseClone", - "ReferenceEquals" - ], - includeGlobalForbidden: false - }; - } - - protected forbiddenForUnionMembers(_: UnionType, unionNamed: Name): ForbiddenWordsInfo { - return { names: [unionNamed], includeGlobalForbidden: true }; - } - - protected makeNamedTypeNamer(): Namer { - return namingFunction; - } - - protected namerForObjectProperty(): Namer { - return this._csOptions.keepPropertyName ? namingFunctionKeep : namingFunction; - } - - protected makeUnionMemberNamer(): Namer { - return namingFunction; - } - - protected makeEnumCaseNamer(): Namer { - return namingFunction; - } - - protected unionNeedsName(u: UnionType): boolean { - return nullableFromUnion(u) === null; - } - - protected namedTypeToNameForTopLevel(type: Type): Type | undefined { - // If the top-level type doesn't contain any classes or unions - // we have to define a class just for the `FromJson` method, in - // emitFromJsonForTopLevel. - return directlyReachableSingleNamedType(type); - } - - protected emitBlock(f: () => void, semicolon = false): void { - this.emitLine("{"); - this.indent(f); - this.emitLine("}", semicolon ? ";" : ""); - } - - protected get doubleType(): string { - return this._csOptions.useDecimal ? "decimal" : "double"; - } - - protected csType(t: Type, follow: (t: Type) => Type = followTargetType, withIssues = false): Sourcelike { - const actualType = follow(t); - return matchType( - actualType, - _anyType => maybeAnnotated(withIssues, anyTypeIssueAnnotation, this._csOptions.typeForAny), - _nullType => maybeAnnotated(withIssues, nullTypeIssueAnnotation, this._csOptions.typeForAny), - _boolType => "bool", - _integerType => "long", - _doubleType => this.doubleType, - _stringType => "string", - arrayType => { - const itemsType = this.csType(arrayType.items, follow, withIssues); - if (this._csOptions.useList) { - return ["List<", itemsType, ">"]; - } else { - return [itemsType, "[]"]; - } - }, - classType => this.nameForNamedType(classType), - mapType => ["Dictionary"], - enumType => this.nameForNamedType(enumType), - unionType => { - const nullable = nullableFromUnion(unionType); - if (nullable !== null) return this.nullableCSType(nullable, noFollow); - return this.nameForNamedType(unionType); - }, - transformedStringType => csTypeForTransformedStringType(transformedStringType) - ); - } - - protected nullableCSType(t: Type, follow: (t: Type) => Type = followTargetType, withIssues = false): Sourcelike { - t = followTargetType(t); - const csType = this.csType(t, follow, withIssues); - if (isValueType(t)) { - return [csType, "?"]; - } else { - return csType; - } - } - - protected baseclassForType(_t: Type): Sourcelike | undefined { - return undefined; - } - - protected emitType( - description: string[] | undefined, - accessModifier: AccessModifier, - declaration: Sourcelike, - name: Sourcelike, - baseclass: Sourcelike | undefined, - emitter: () => void - ): void { - switch (accessModifier) { - case AccessModifier.Public: - declaration = ["public ", declaration]; - break; - case AccessModifier.Internal: - declaration = ["internal ", declaration]; - break; - default: - break; - } - - this.emitDescription(description); - if (baseclass === undefined) { - this.emitLine(declaration, " ", name); - } else { - this.emitLine(declaration, " ", name, " : ", baseclass); - } - - this.emitBlock(emitter); - } - - protected attributesForProperty( - _property: ClassProperty, - _name: Name, - _c: ClassType, - _jsonName: string - ): Sourcelike[] | undefined { - return undefined; - } - - protected propertyDefinition(property: ClassProperty, name: Name, _c: ClassType, _jsonName: string): Sourcelike { - const t = property.type; - const csType = property.isOptional - ? this.nullableCSType(t, followTargetType, true) - : this.csType(t, followTargetType, true); - - const propertyArray = ["public "]; - - if (this._csOptions.virtual) propertyArray.push("virtual "); - - return [...propertyArray, csType, " ", name, " { get; set; }"]; - } - - protected emitDescriptionBlock(lines: Sourcelike[]): void { - const start = "/// "; - if (this._csOptions.dense) { - this.emitLine(start, lines.join("; "), ""); - } else { - this.emitCommentLines(lines, { lineStart: "/// ", beforeComment: start, afterComment: "/// " }); - } - } - - protected blankLinesBetweenAttributes(): boolean { - return false; - } - - private emitClassDefinition(c: ClassType, className: Name): void { - this.emitType( - this.descriptionForType(c), - AccessModifier.Public, - "partial class", - className, - this.baseclassForType(c), - () => { - if (c.getProperties().size === 0) return; - const blankLines = this.blankLinesBetweenAttributes() ? "interposing" : "none"; - let columns: Sourcelike[][] = []; - let isFirstProperty = true; - let previousDescription: string[] | undefined = undefined; - this.forEachClassProperty(c, blankLines, (name, jsonName, p) => { - const attributes = this.attributesForProperty(p, name, c, jsonName); - const description = this.descriptionForClassProperty(c, jsonName); - const property = this.propertyDefinition(p, name, c, jsonName); - if (attributes === undefined) { - if ( - // Descriptions should be preceded by an empty line - (!isFirstProperty && description !== undefined) || - // If the previous property has a description, leave an empty line - previousDescription !== undefined - ) { - this.ensureBlankLine(); - } - - this.emitDescription(description); - this.emitLine(property); - } else if (this._csOptions.dense && attributes.length > 0) { - const comment = description === undefined ? "" : ` // ${description.join("; ")}`; - columns.push([attributes, " ", property, comment]); - } else { - this.emitDescription(description); - for (const attribute of attributes) { - this.emitLine(attribute); - } - - this.emitLine(property); - } - - isFirstProperty = false; - previousDescription = description; - }); - if (columns.length > 0) { - this.emitTable(columns); - } - } - ); - } - - private emitUnionDefinition(u: UnionType, unionName: Name): void { - const nonNulls = removeNullFromUnion(u, true)[1]; - this.emitType( - this.descriptionForType(u), - AccessModifier.Public, - "partial struct", - unionName, - this.baseclassForType(u), - () => { - this.forEachUnionMember(u, nonNulls, "none", null, (fieldName, t) => { - const csType = this.nullableCSType(t); - this.emitLine("public ", csType, " ", fieldName, ";"); - }); - this.ensureBlankLine(); - const nullTests: Sourcelike[] = Array.from(nonNulls).map(t => [ - this.nameForUnionMember(u, t), - " == null" - ]); - this.ensureBlankLine(); - this.forEachUnionMember(u, nonNulls, "none", null, (fieldName, t) => { - const csType = this.csType(t); - this.emitExpressionMember( - ["public static implicit operator ", unionName, "(", csType, " ", fieldName, ")"], - ["new ", unionName, " { ", fieldName, " = ", fieldName, " }"] - ); - }); - if (u.findMember("null") === undefined) return; - this.emitExpressionMember("public bool IsNull", arrayIntercalate(" && ", nullTests), true); - } - ); - } - - private emitEnumDefinition(e: EnumType, enumName: Name): void { - const caseNames: Sourcelike[] = []; - this.forEachEnumCase(e, "none", name => { - if (caseNames.length > 0) caseNames.push(", "); - caseNames.push(name); - }); - this.emitDescription(this.descriptionForType(e)); - this.emitLine("public enum ", enumName, " { ", caseNames, " };"); - } - - protected emitExpressionMember(declare: Sourcelike, define: Sourcelike, isProperty = false): void { - if (this._csOptions.version === 5) { - this.emitLine(declare); - this.emitBlock(() => { - const stmt = ["return ", define, ";"]; - if (isProperty) { - this.emitLine("get"); - this.emitBlock(() => this.emitLine(stmt)); - } else { - this.emitLine(stmt); - } - }); - } else { - this.emitLine(declare, " => ", define, ";"); - } - } - - protected emitTypeSwitch( - types: Iterable, - condition: (t: T) => Sourcelike, - withBlock: boolean, - withReturn: boolean, - f: (t: T) => void - ): void { - assert(!withReturn || withBlock, "Can only have return with block"); - for (const t of types) { - this.emitLine("if (", condition(t), ")"); - if (withBlock) { - this.emitBlock(() => { - f(t); - if (withReturn) { - this.emitLine("return;"); - } - }); - } else { - this.indent(() => f(t)); - } - } - } - - protected emitUsing(ns: Sourcelike): void { - this.emitLine("using ", ns, ";"); - } - - protected emitUsings(): void { - for (const ns of ["System", "System.Collections.Generic"]) { - this.emitUsing(ns); - } - } - - protected emitRequiredHelpers(): void { - return; - } - - private emitTypesAndSupport(): void { - this.forEachObject("leading-and-interposing", (c: ClassType, name: Name) => this.emitClassDefinition(c, name)); - this.forEachEnum("leading-and-interposing", (e, name) => this.emitEnumDefinition(e, name)); - this.forEachUnion("leading-and-interposing", (u, name) => this.emitUnionDefinition(u, name)); - this.emitRequiredHelpers(); - } - - protected emitDefaultLeadingComments(): void { - return; - } - - protected emitDefaultFollowingComments(): void { - return; - } - - protected needNamespace(): boolean { - return true; - } - - protected emitSourceStructure(): void { - if (this.leadingComments !== undefined) { - this.emitComments(this.leadingComments); - } else { - this.emitDefaultLeadingComments(); - } - - this.ensureBlankLine(); - if (this.needNamespace()) { - this.emitLine("namespace ", this._csOptions.namespace); - this.emitBlock(() => { - this.emitUsings(); - this.emitTypesAndSupport(); - }); - } else { - this.emitUsings(); - this.emitTypesAndSupport(); - } - - this.emitDefaultFollowingComments(); - } -} - -export const newtonsoftCSharpOptions = Object.assign({}, cSharpOptions, {}); - -export class NewtonsoftCSharpRenderer extends CSharpRenderer { - private readonly _enumExtensionsNames = new Map(); - - private readonly _needHelpers: boolean; - - private readonly _needAttributes: boolean; - - private readonly _needNamespaces: boolean; - - public constructor( - targetLanguage: TargetLanguage, - renderContext: RenderContext, - private readonly _options: OptionValues - ) { - super(targetLanguage, renderContext, _options); - this._needHelpers = _options.features.helpers; - this._needAttributes = _options.features.attributes; - this._needNamespaces = _options.features.namespaces; - } - - protected forbiddenNamesForGlobalNamespace(): string[] { - const forbidden = [ - "Converter", - "JsonConverter", - "JsonSerializer", - "JsonWriter", - "JsonToken", - "Serialize", - "Newtonsoft", - "MetadataPropertyHandling", - "DateParseHandling", - "FromJson", - "Required" - ]; - if (this._options.dense) { - forbidden.push("J", "R", "N"); - } - - if (this._options.baseclass !== undefined) { - forbidden.push(this._options.baseclass); - } - - return super.forbiddenNamesForGlobalNamespace().concat(forbidden); - } - - protected forbiddenForObjectProperties(c: ClassType, className: Name): ForbiddenWordsInfo { - const result = super.forbiddenForObjectProperties(c, className); - result.names = result.names.concat(["ToJson", "FromJson", "Required"]); - return result; - } - - protected makeNameForTransformation(xf: Transformation, typeName: Name | undefined): Name { - if (typeName === undefined) { - let xfer = xf.transformer; - if (xfer instanceof DecodingTransformer && xfer.consumer !== undefined) { - xfer = xfer.consumer; - } - - return new SimpleName([`${xfer.kind}_converter`], namingFunction, inferredNameOrder + 30); - } - - return new DependencyName(namingFunction, typeName.order + 30, lookup => `${lookup(typeName)}_converter`); - } - - protected makeNamedTypeDependencyNames(t: Type, name: Name): DependencyName[] { - if (!(t instanceof EnumType)) return []; - - const extensionsName = new DependencyName( - namingFunction, - name.order + 30, - lookup => `${lookup(name)}_extensions` - ); - this._enumExtensionsNames.set(name, extensionsName); - return [extensionsName]; - } - - protected emitUsings(): void { - // FIXME: We need System.Collections.Generic whenever we have maps or use List. - if (!this._needAttributes && !this._needHelpers) return; - - super.emitUsings(); - this.ensureBlankLine(); - - for (const ns of ["System.Globalization", "Newtonsoft.Json", "Newtonsoft.Json.Converters"]) { - this.emitUsing(ns); - } - - if (this._options.dense) { - this.emitUsing([denseJsonPropertyName, " = Newtonsoft.Json.JsonPropertyAttribute"]); - this.emitUsing([denseRequiredEnumName, " = Newtonsoft.Json.Required"]); - this.emitUsing([denseNullValueHandlingEnumName, " = Newtonsoft.Json.NullValueHandling"]); - } - - if (this._options.baseclass === "EntityData") { - this.emitUsing("Microsoft.Azure.Mobile.Server"); - } - } - - protected baseclassForType(_t: Type): Sourcelike | undefined { - return this._options.baseclass; - } - - protected emitDefaultLeadingComments(): void { - if (!this._needHelpers) return; - - this.emitLine("// "); - this.emitLine("//"); - this.emitLine( - "// To parse this JSON data, add NuGet 'Newtonsoft.Json' then do", - this.topLevels.size === 1 ? "" : " one of these", - ":" - ); - this.emitLine("//"); - this.emitLine("// using ", this._options.namespace, ";"); - this.emitLine("//"); - this.forEachTopLevel("none", (t, topLevelName) => { - let rhs: Sourcelike; - if (t instanceof EnumType) { - rhs = ["JsonConvert.DeserializeObject<", topLevelName, ">(jsonString)"]; - } else { - rhs = [topLevelName, ".FromJson(jsonString)"]; - } - - this.emitLine("// var ", modifySource(camelCase, topLevelName), " = ", rhs, ";"); - }); - } - - private converterForType(t: Type): Name | undefined { - let xf = transformationForType(t); - - if (xf === undefined && t instanceof UnionType) { - const maybeNullable = nullableFromUnion(t); - if (maybeNullable !== null) { - t = maybeNullable; - xf = transformationForType(t); - } - } - - if (xf === undefined) return undefined; - - if (alwaysApplyTransformation(xf)) return undefined; - - return defined(this.nameForTransformation(t)); - } - - protected attributesForProperty( - property: ClassProperty, - _name: Name, - _c: ClassType, - jsonName: string - ): Sourcelike[] | undefined { - if (!this._needAttributes) return undefined; - - const attributes: Sourcelike[] = []; - - const jsonProperty = this._options.dense ? denseJsonPropertyName : "JsonProperty"; - const escapedName = utf16StringEscape(jsonName); - const isNullable = followTargetType(property.type).isNullable; - const isOptional = property.isOptional; - const requiredClass = this._options.dense ? "R" : "Required"; - const nullValueHandlingClass = this._options.dense ? "N" : "NullValueHandling"; - const nullValueHandling = - isOptional && !isNullable ? [", NullValueHandling = ", nullValueHandlingClass, ".Ignore"] : []; - let required: Sourcelike; - if (!this._options.checkRequired || (isOptional && isNullable)) { - required = [nullValueHandling]; - } else if (isOptional && !isNullable) { - required = [", Required = ", requiredClass, ".DisallowNull", nullValueHandling]; - } else if (!isOptional && isNullable) { - required = [", Required = ", requiredClass, ".AllowNull"]; - } else { - required = [", Required = ", requiredClass, ".Always", nullValueHandling]; - } - - attributes.push(["[", jsonProperty, '("', escapedName, '"', required, ")]"]); - - const converter = this.converterForType(property.type); - if (converter !== undefined) { - attributes.push(["[JsonConverter(typeof(", converter, "))]"]); - } - - return attributes; - } - - protected blankLinesBetweenAttributes(): boolean { - return this._needAttributes && !this._options.dense; - } - - // The "this" type can't be `dynamic`, so we have to force it to `object`. - private topLevelResultType(t: Type): Sourcelike { - return t.kind === "any" || t.kind === "none" ? "object" : this.csType(t); - } - - private emitFromJsonForTopLevel(t: Type, name: Name): void { - if (t instanceof EnumType) return; - - let partial: string; - let typeKind: string; - const definedType = this.namedTypeToNameForTopLevel(t); - if (definedType !== undefined) { - partial = "partial "; - typeKind = definedType instanceof ClassType ? "class" : "struct"; - } else { - partial = ""; - typeKind = "class"; - } - - const csType = this.topLevelResultType(t); - this.emitType(undefined, AccessModifier.Public, [partial, typeKind], name, this.baseclassForType(t), () => { - // FIXME: Make FromJson a Named - this.emitExpressionMember( - ["public static ", csType, " FromJson(string json)"], - ["JsonConvert.DeserializeObject<", csType, ">(json, ", this._options.namespace, ".Converter.Settings)"] - ); - }); - } - - private emitDecoderSwitch(emitBody: () => void): void { - this.emitLine("switch (reader.TokenType)"); - this.emitBlock(emitBody); - } - - private emitTokenCase(tokenType: string): void { - this.emitLine("case JsonToken.", tokenType, ":"); - } - - private emitThrow(message: Sourcelike): void { - this.emitLine("throw new Exception(", message, ");"); - } - - private deserializeTypeCode(typeName: Sourcelike): Sourcelike { - return ["serializer.Deserialize<", typeName, ">(reader)"]; - } - - private serializeValueCode(value: Sourcelike): Sourcelike { - return ["serializer.Serialize(writer, ", value, ")"]; - } - - private emitSerializeClass(): void { - // FIXME: Make Serialize a Named - this.emitType(undefined, AccessModifier.Public, "static class", "Serialize", undefined, () => { - // Sometimes multiple top-levels will resolve to the same type, so we have to take care - // not to emit more than one extension method for the same type. - const seenTypes = new Set(); - this.forEachTopLevel("none", t => { - // FIXME: Make ToJson a Named - if (!seenTypes.has(t)) { - seenTypes.add(t); - this.emitExpressionMember( - ["public static string ToJson(this ", this.topLevelResultType(t), " self)"], - ["JsonConvert.SerializeObject(self, ", this._options.namespace, ".Converter.Settings)"] - ); - } - }); - }); - } - - private emitCanConvert(expr: Sourcelike): void { - this.emitExpressionMember("public override bool CanConvert(Type t)", expr); - } - - private emitReadJson(emitBody: () => void): void { - this.emitLine( - "public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer)" - ); - this.emitBlock(emitBody); - } - - private emitWriteJson(variable: string, emitBody: () => void): void { - this.emitLine( - "public override void WriteJson(JsonWriter writer, object ", - variable, - ", JsonSerializer serializer)" - ); - this.emitBlock(emitBody); - } - - private converterObject(converterName: Name): Sourcelike { - // FIXME: Get a singleton - return [converterName, ".Singleton"]; - } - - private emitConverterClass(): void { - // FIXME: Make Converter a Named - const converterName: Sourcelike = ["Converter"]; - this.emitType(undefined, AccessModifier.Internal, "static class", converterName, undefined, () => { - this.emitLine("public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings"); - this.emitBlock(() => { - this.emitLine("MetadataPropertyHandling = MetadataPropertyHandling.Ignore,"); - this.emitLine("DateParseHandling = DateParseHandling.None,"); - this.emitLine("Converters ="); - this.emitLine("{"); - this.indent(() => { - for (const [t, converter] of this.typesWithNamedTransformations) { - if (alwaysApplyTransformation(defined(transformationForType(t)))) { - this.emitLine(this.converterObject(converter), ","); - } - } - - this.emitLine("new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }"); - }); - this.emitLine("},"); - }, true); - }); - } - - private emitDecoderTransformerCase( - tokenCases: string[], - variableName: string, - xfer: Transformer | undefined, - targetType: Type, - emitFinish: (value: Sourcelike) => void - ): void { - if (xfer === undefined) return; - - for (const tokenCase of tokenCases) { - this.emitTokenCase(tokenCase); - } - - this.indent(() => { - const allHandled = this.emitDecodeTransformer(xfer, targetType, emitFinish, variableName); - if (!allHandled) { - this.emitLine("break;"); - } - }); - } - - private emitConsume( - value: Sourcelike, - consumer: Transformer | undefined, - targetType: Type, - emitFinish: (variableName: Sourcelike) => void - ): boolean { - if (consumer === undefined) { - emitFinish(value); - return true; - } else { - return this.emitTransformer(value, consumer, targetType, emitFinish); - } - } - - private emitDecodeTransformer( - xfer: Transformer, - targetType: Type, - emitFinish: (value: Sourcelike) => void, - variableName = "value" - ): boolean { - if (xfer instanceof DecodingTransformer) { - const source = xfer.sourceType; - const converter = this.converterForType(targetType); - if (converter !== undefined) { - const typeSource = this.csType(targetType); - this.emitLine("var converter = ", this.converterObject(converter), ";"); - this.emitLine( - "var ", - variableName, - " = (", - typeSource, - ")converter.ReadJson(reader, typeof(", - typeSource, - "), null, serializer);" - ); - } else if (source.kind !== "null") { - let output = targetType.kind === "double" ? targetType : source; - this.emitLine("var ", variableName, " = ", this.deserializeTypeCode(this.csType(output)), ";"); - } - - return this.emitConsume(variableName, xfer.consumer, targetType, emitFinish); - } else if (xfer instanceof ArrayDecodingTransformer) { - // FIXME: Consume StartArray - if (!(targetType instanceof ArrayType)) { - return panic("Array decoding must produce an array type"); - } - - // FIXME: handle EOF - this.emitLine("reader.Read();"); - this.emitLine("var ", variableName, " = new List<", this.csType(targetType.items), ">();"); - this.emitLine("while (reader.TokenType != JsonToken.EndArray)"); - this.emitBlock(() => { - this.emitDecodeTransformer( - xfer.itemTransformer, - xfer.itemTargetType, - v => this.emitLine(variableName, ".Add(", v, ");"), - "arrayItem" - ); - // FIXME: handle EOF - this.emitLine("reader.Read();"); - }); - let result: Sourcelike = variableName; - if (!this._options.useList) { - result = [result, ".ToArray()"]; - } - - emitFinish(result); - return true; - } else if (xfer instanceof DecodingChoiceTransformer) { - this.emitDecoderSwitch(() => { - const nullTransformer = xfer.nullTransformer; - if (nullTransformer !== undefined) { - this.emitTokenCase("Null"); - this.indent(() => { - const allHandled = this.emitDecodeTransformer(nullTransformer, targetType, emitFinish, "null"); - if (!allHandled) { - this.emitLine("break"); - } - }); - } - - this.emitDecoderTransformerCase( - ["Integer"], - "integerValue", - xfer.integerTransformer, - targetType, - emitFinish - ); - this.emitDecoderTransformerCase( - xfer.integerTransformer === undefined ? ["Integer", "Float"] : ["Float"], - "doubleValue", - xfer.doubleTransformer, - targetType, - emitFinish - ); - this.emitDecoderTransformerCase(["Boolean"], "boolValue", xfer.boolTransformer, targetType, emitFinish); - this.emitDecoderTransformerCase( - ["String", "Date"], - "stringValue", - xfer.stringTransformer, - targetType, - emitFinish - ); - this.emitDecoderTransformerCase( - ["StartObject"], - "objectValue", - xfer.objectTransformer, - targetType, - emitFinish - ); - this.emitDecoderTransformerCase( - ["StartArray"], - "arrayValue", - xfer.arrayTransformer, - targetType, - emitFinish - ); - }); - return false; - } else { - return panic("Unknown transformer"); - } - } - - private stringCaseValue(t: Type, stringCase: string): Sourcelike { - if (t.kind === "string") { - return ['"', utf16StringEscape(stringCase), '"']; - } else if (t instanceof EnumType) { - return [this.nameForNamedType(t), ".", this.nameForEnumCase(t, stringCase)]; - } - - return panic(`Type ${t.kind} does not have string cases`); - } - - private emitTransformer( - variable: Sourcelike, - xfer: Transformer, - targetType: Type, - emitFinish: (value: Sourcelike) => void - ): boolean { - function directTargetType(continuation: Transformer | undefined): Type { - if (continuation === undefined) { - return targetType; - } - - return followTargetType(continuation.sourceType); - } - - if (xfer instanceof ChoiceTransformer) { - const caseXfers = xfer.transformers; - if (caseXfers.length > 1 && caseXfers.every(caseXfer => caseXfer instanceof StringMatchTransformer)) { - this.emitLine("switch (", variable, ")"); - this.emitBlock(() => { - for (const caseXfer of caseXfers) { - const matchXfer = caseXfer as StringMatchTransformer; - const value = this.stringCaseValue( - followTargetType(matchXfer.sourceType), - matchXfer.stringCase - ); - this.emitLine("case ", value, ":"); - this.indent(() => { - const allDone = this.emitTransformer( - variable, - matchXfer.transformer, - targetType, - emitFinish - ); - if (!allDone) { - this.emitLine("break;"); - } - }); - } - }); - // FIXME: Can we check for exhaustiveness? For enums it should be easy. - return false; - } else { - for (const caseXfer of caseXfers) { - this.emitTransformer(variable, caseXfer, targetType, emitFinish); - } - } - } else if (xfer instanceof UnionMemberMatchTransformer) { - const memberType = xfer.memberType; - const maybeNullable = nullableFromUnion(xfer.sourceType); - let test: Sourcelike; - let member: Sourcelike; - if (maybeNullable !== null) { - if (memberType.kind === "null") { - test = [variable, " == null"]; - member = "null"; - } else { - test = [variable, " != null"]; - member = variable; - } - } else if (memberType.kind === "null") { - test = [variable, ".IsNull"]; - member = "null"; - } else { - const memberName = this.nameForUnionMember(xfer.sourceType, memberType); - member = [variable, ".", memberName]; - test = [member, " != null"]; - } - - if (memberType.kind !== "null" && isValueType(memberType)) { - member = [member, ".Value"]; - } - - this.emitLine("if (", test, ")"); - this.emitBlock(() => this.emitTransformer(member, xfer.transformer, targetType, emitFinish)); - } else if (xfer instanceof StringMatchTransformer) { - const value = this.stringCaseValue(followTargetType(xfer.sourceType), xfer.stringCase); - this.emitLine("if (", variable, " == ", value, ")"); - this.emitBlock(() => this.emitTransformer(variable, xfer.transformer, targetType, emitFinish)); - } else if (xfer instanceof EncodingTransformer) { - const converter = this.converterForType(xfer.sourceType); - if (converter !== undefined) { - this.emitLine("var converter = ", this.converterObject(converter), ";"); - this.emitLine("converter.WriteJson(writer, ", variable, ", serializer);"); - } else { - this.emitLine(this.serializeValueCode(variable), ";"); - } - - emitFinish([]); - return true; - } else if (xfer instanceof ArrayEncodingTransformer) { - this.emitLine("writer.WriteStartArray();"); - const itemVariable = "arrayItem"; - this.emitLine("foreach (var ", itemVariable, " in ", variable, ")"); - this.emitBlock(() => { - this.emitTransformer(itemVariable, xfer.itemTransformer, xfer.itemTargetType, () => { - return; - }); - }); - this.emitLine("writer.WriteEndArray();"); - emitFinish([]); - return true; - } else if (xfer instanceof ParseStringTransformer) { - const immediateTargetType = xfer.consumer === undefined ? targetType : xfer.consumer.sourceType; - switch (immediateTargetType.kind) { - case "date-time": - this.emitLine("DateTimeOffset dt;"); - this.emitLine("if (DateTimeOffset.TryParse(", variable, ", out dt))"); - this.emitBlock(() => this.emitConsume("dt", xfer.consumer, targetType, emitFinish)); - break; - case "uuid": - this.emitLine("Guid guid;"); - this.emitLine("if (Guid.TryParse(", variable, ", out guid))"); - this.emitBlock(() => this.emitConsume("guid", xfer.consumer, targetType, emitFinish)); - break; - case "uri": - this.emitLine("try"); - this.emitBlock(() => { - this.emitLine("var uri = new Uri(", variable, ");"); - this.emitConsume("uri", xfer.consumer, targetType, emitFinish); - }); - this.emitLine("catch (UriFormatException) {}"); - break; - case "integer": - this.emitLine("long l;"); - this.emitLine("if (Int64.TryParse(", variable, ", out l))"); - this.emitBlock(() => this.emitConsume("l", xfer.consumer, targetType, emitFinish)); - break; - case "bool": - this.emitLine("bool b;"); - this.emitLine("if (Boolean.TryParse(", variable, ", out b))"); - this.emitBlock(() => this.emitConsume("b", xfer.consumer, targetType, emitFinish)); - break; - default: - return panic(`Parsing string to ${immediateTargetType.kind} not supported`); - } - } else if (xfer instanceof StringifyTransformer) { - switch (xfer.sourceType.kind) { - case "date-time": - return this.emitConsume( - [variable, '.ToString("o", System.Globalization.CultureInfo.InvariantCulture)'], - xfer.consumer, - targetType, - emitFinish - ); - case "uuid": - return this.emitConsume( - [variable, '.ToString("D", System.Globalization.CultureInfo.InvariantCulture)'], - xfer.consumer, - targetType, - emitFinish - ); - case "integer": - case "uri": - return this.emitConsume([variable, ".ToString()"], xfer.consumer, targetType, emitFinish); - case "bool": - this.emitLine("var boolString = ", variable, ' ? "true" : "false";'); - return this.emitConsume("boolString", xfer.consumer, targetType, emitFinish); - default: - return panic(`Stringifying ${xfer.sourceType.kind} not supported`); - } - } else if (xfer instanceof StringProducerTransformer) { - const value = this.stringCaseValue(directTargetType(xfer.consumer), xfer.result); - return this.emitConsume(value, xfer.consumer, targetType, emitFinish); - } else if (xfer instanceof MinMaxLengthCheckTransformer) { - const min = xfer.minLength; - const max = xfer.maxLength; - const conditions: Sourcelike[] = []; - - if (min !== undefined) { - conditions.push([variable, ".Length >= ", min.toString()]); - } - - if (max !== undefined) { - conditions.push([variable, ".Length <= ", max.toString()]); - } - - this.emitLine("if (", arrayIntercalate([" && "], conditions), ")"); - this.emitBlock(() => this.emitConsume(variable, xfer.consumer, targetType, emitFinish)); - return false; - } else if (xfer instanceof MinMaxValueTransformer) { - const min = xfer.minimum; - const max = xfer.maximum; - const conditions: Sourcelike[] = []; - - if (min !== undefined) { - conditions.push([variable, " >= ", min.toString()]); - } - - if (max !== undefined) { - conditions.push([variable, " <= ", max.toString()]); - } - - this.emitLine("if (", arrayIntercalate([" && "], conditions), ")"); - this.emitBlock(() => this.emitConsume(variable, xfer.consumer, targetType, emitFinish)); - return false; - } else if (xfer instanceof UnionInstantiationTransformer) { - if (!(targetType instanceof UnionType)) { - return panic("Union instantiation transformer must produce a union type"); - } - - const maybeNullable = nullableFromUnion(targetType); - if (maybeNullable !== null) { - emitFinish(variable); - } else { - const unionName = this.nameForNamedType(targetType); - let initializer: Sourcelike; - if (xfer.sourceType.kind === "null") { - initializer = " "; - } else { - const memberName = this.nameForUnionMember(targetType, xfer.sourceType); - initializer = [" ", memberName, " = ", variable, " "]; - } - - emitFinish(["new ", unionName, " {", initializer, "}"]); - } - - return true; - } else { - return panic("Unknown transformer"); - } - - return false; - } - - private emitTransformation(converterName: Name, t: Type): void { - const xf = defined(transformationForType(t)); - const reverse = xf.reverse; - const targetType = xf.targetType; - const xfer = xf.transformer; - this.emitType(undefined, AccessModifier.Internal, "class", converterName, "JsonConverter", () => { - const csType = this.csType(targetType); - let canConvertExpr: Sourcelike = ["t == typeof(", csType, ")"]; - const haveNullable = isValueType(targetType); - if (haveNullable) { - canConvertExpr = [canConvertExpr, " || t == typeof(", csType, "?)"]; - } - - this.emitCanConvert(canConvertExpr); - this.ensureBlankLine(); - this.emitReadJson(() => { - // FIXME: It's unsatisfying that we need this. The reason is that we not - // only match T, but also T?. If we didn't, then the T in T? would not be - // deserialized with our converter but with the default one. Can we check - // whether the type is a nullable? - // FIXME: This could duplicate one of the cases handled below in - // `emitDecodeTransformer`. - if (haveNullable && !(targetType instanceof UnionType)) { - this.emitLine("if (reader.TokenType == JsonToken.Null) return null;"); - } - - const allHandled = this.emitDecodeTransformer(xfer, targetType, v => this.emitLine("return ", v, ";")); - if (!allHandled) { - this.emitThrow(['"Cannot unmarshal type ', csType, '"']); - } - }); - this.ensureBlankLine(); - this.emitWriteJson("untypedValue", () => { - // FIXME: See above. - if (haveNullable && !(targetType instanceof UnionType)) { - this.emitLine("if (untypedValue == null)"); - this.emitBlock(() => { - this.emitLine("serializer.Serialize(writer, null);"); - this.emitLine("return;"); - }); - } - - this.emitLine("var value = (", csType, ")untypedValue;"); - const allHandled = this.emitTransformer("value", reverse.transformer, reverse.targetType, () => - this.emitLine("return;") - ); - if (!allHandled) { - this.emitThrow(['"Cannot marshal type ', csType, '"']); - } - }); - this.ensureBlankLine(); - this.emitLine("public static readonly ", converterName, " Singleton = new ", converterName, "();"); - }); - } - - protected emitRequiredHelpers(): void { - if (this._needHelpers) { - this.forEachTopLevel("leading-and-interposing", (t, n) => this.emitFromJsonForTopLevel(t, n)); - this.ensureBlankLine(); - this.emitSerializeClass(); - } - - if (this._needHelpers || (this._needAttributes && (this.haveNamedUnions || this.haveEnums))) { - this.ensureBlankLine(); - this.emitConverterClass(); - this.forEachTransformation("leading-and-interposing", (n, t) => this.emitTransformation(n, t)); - } - } - - protected needNamespace(): boolean { - return this._needNamespaces; - } -} - -export const systemTextJsonCSharpOptions = Object.assign({}, cSharpOptions, {}); - -export class SystemTextJsonCSharpRenderer extends CSharpRenderer { - private readonly _enumExtensionsNames = new Map(); - - private readonly _needHelpers: boolean; - - private readonly _needAttributes: boolean; - - private readonly _needNamespaces: boolean; - - public constructor( - targetLanguage: TargetLanguage, - renderContext: RenderContext, - private readonly _options: OptionValues - ) { - super(targetLanguage, renderContext, _options); - this._needHelpers = _options.features.helpers; - this._needAttributes = _options.features.attributes; - this._needNamespaces = _options.features.namespaces; - } - - protected forbiddenNamesForGlobalNamespace(): string[] { - const forbidden = [ - "Converter", - "JsonConverter", - "JsonSerializer", - "JsonWriter", - "JsonToken", - "Serialize", - "JsonSerializerOptions", - // "Newtonsoft", - // "MetadataPropertyHandling", - // "DateParseHandling", - "FromJson", - "Required" - ]; - if (this._options.dense) { - forbidden.push("J", "R", "N"); - } - - if (this._options.baseclass !== undefined) { - forbidden.push(this._options.baseclass); - } - - return super.forbiddenNamesForGlobalNamespace().concat(forbidden); - } - - protected forbiddenForObjectProperties(c: ClassType, className: Name): ForbiddenWordsInfo { - const result = super.forbiddenForObjectProperties(c, className); - result.names = result.names.concat(["ToJson", "FromJson", "Required"]); - return result; - } - - protected makeNameForTransformation(xf: Transformation, typeName: Name | undefined): Name { - if (typeName === undefined) { - let xfer = xf.transformer; - if (xfer instanceof DecodingTransformer && xfer.consumer !== undefined) { - xfer = xfer.consumer; - } - - return new SimpleName([`${xfer.kind}_converter`], namingFunction, inferredNameOrder + 30); - } - - return new DependencyName(namingFunction, typeName.order + 30, lookup => `${lookup(typeName)}_converter`); - } - - protected makeNamedTypeDependencyNames(t: Type, name: Name): DependencyName[] { - if (!(t instanceof EnumType)) return []; - - const extensionsName = new DependencyName( - namingFunction, - name.order + 30, - lookup => `${lookup(name)}_extensions` - ); - this._enumExtensionsNames.set(name, extensionsName); - return [extensionsName]; - } - - protected emitUsings(): void { - // FIXME: We need System.Collections.Generic whenever we have maps or use List. - if (!this._needAttributes && !this._needHelpers) return; - - super.emitUsings(); - this.ensureBlankLine(); - - for (const ns of ["System.Text.Json", "System.Text.Json.Serialization", "System.Globalization"]) { - this.emitUsing(ns); - } - - if (this._options.dense) { - this.emitUsing([denseJsonPropertyName, " = System.Text.Json.Serialization.JsonPropertyNameAttribute"]); - // this.emitUsing([denseRequiredEnumName, " = Newtonsoft.Json.Required"]); - this.emitUsing([denseNullValueHandlingEnumName, " = System.Text.Json.Serialization.JsonIgnoreCondition"]); - } - - if (this._options.baseclass === "EntityData") { - this.emitUsing("Microsoft.Azure.Mobile.Server"); - } - } - - protected baseclassForType(_t: Type): Sourcelike | undefined { - return this._options.baseclass; - } - - protected emitDefaultFollowingComments(): void { - if (!this._needHelpers) return; - - this.emitLine("#pragma warning restore CS8618"); - this.emitLine("#pragma warning restore CS8601"); - this.emitLine("#pragma warning restore CS8603"); - } - - protected emitDefaultLeadingComments(): void { - if (!this._needHelpers) return; - - this.emitLine("// "); - this.emitLine("//"); - this.emitLine( - "// To parse this JSON data, add NuGet 'System.Text.Json' then do", - this.topLevels.size === 1 ? "" : " one of these", - ":" - ); - this.emitLine("//"); - this.emitLine("// using ", this._options.namespace, ";"); - this.emitLine("//"); - this.forEachTopLevel("none", (t, topLevelName) => { - let rhs: Sourcelike; - if (t instanceof EnumType) { - rhs = ["JsonSerializer.Deserialize<", topLevelName, ">(jsonString)"]; - } else { - rhs = [topLevelName, ".FromJson(jsonString)"]; - } - - this.emitLine("// var ", modifySource(camelCase, topLevelName), " = ", rhs, ";"); - }); - - // fix: should this be an option? Or respond to an existing option? - this.emitLine("#nullable enable"); - this.emitLine("#pragma warning disable CS8618"); - this.emitLine("#pragma warning disable CS8601"); - this.emitLine("#pragma warning disable CS8603"); - } - - private converterForType(t: Type): Name | undefined { - let xf = transformationForType(t); - - if (xf === undefined && t instanceof UnionType) { - const maybeNullable = nullableFromUnion(t); - if (maybeNullable !== null) { - t = maybeNullable; - xf = transformationForType(t); - } - } - - if (xf === undefined) return undefined; - - if (alwaysApplyTransformation(xf)) return undefined; - - return defined(this.nameForTransformation(t)); - } - - protected attributesForProperty( - property: ClassProperty, - _name: Name, - _c: ClassType, - jsonName: string - ): Sourcelike[] | undefined { - if (!this._needAttributes) return undefined; - - const attributes: Sourcelike[] = []; - - const jsonPropertyName = this._options.dense ? denseJsonPropertyName : "JsonPropertyName"; - const escapedName = utf16StringEscape(jsonName); - const isNullable = followTargetType(property.type).isNullable; - const isOptional = property.isOptional; - - if (isOptional && !isNullable) { - attributes.push(["[", "JsonIgnore", "(Condition = JsonIgnoreCondition.WhenWritingNull)]"]); - } - - // const requiredClass = this._options.dense ? "R" : "Required"; - // const nullValueHandlingClass = this._options.dense ? "N" : "NullValueHandling"; - // const nullValueHandling = isOptional && !isNullable ? [", NullValueHandling = ", nullValueHandlingClass, ".Ignore"] : []; - // let required: Sourcelike; - // if (!this._options.checkRequired || (isOptional && isNullable)) { - // required = [nullValueHandling]; - // } else if (isOptional && !isNullable) { - // required = [", Required = ", requiredClass, ".DisallowNull", nullValueHandling]; - // } else if (!isOptional && isNullable) { - // required = [", Required = ", requiredClass, ".AllowNull"]; - // } else { - // required = [", Required = ", requiredClass, ".Always", nullValueHandling]; - // } - - attributes.push(["[", jsonPropertyName, '("', escapedName, '")]']); - - const converter = this.converterForType(property.type); - if (converter !== undefined) { - attributes.push(["[JsonConverter(typeof(", converter, "))]"]); - } - - return attributes; - } - - protected blankLinesBetweenAttributes(): boolean { - return this._needAttributes && !this._options.dense; - } - - // The "this" type can't be `dynamic`, so we have to force it to `object`. - private topLevelResultType(t: Type): Sourcelike { - return t.kind === "any" || t.kind === "none" ? "object" : this.csType(t); - } - - private emitFromJsonForTopLevel(t: Type, name: Name): void { - if (t instanceof EnumType) return; - - let partial: string; - let typeKind: string; - const definedType = this.namedTypeToNameForTopLevel(t); - if (definedType !== undefined) { - partial = "partial "; - typeKind = definedType instanceof ClassType ? "class" : "struct"; - } else { - partial = ""; - typeKind = "class"; - } - - const csType = this.topLevelResultType(t); - this.emitType(undefined, AccessModifier.Public, [partial, typeKind], name, this.baseclassForType(t), () => { - // FIXME: Make FromJson a Named - this.emitExpressionMember( - ["public static ", csType, " FromJson(string json)"], - ["JsonSerializer.Deserialize<", csType, ">(json, ", this._options.namespace, ".Converter.Settings)"] - ); - }); - } - - private emitDecoderSwitch(emitBody: () => void): void { - this.emitLine("switch (reader.TokenType)"); - this.emitBlock(emitBody); - } - - private emitTokenCase(tokenType: string): void { - this.emitLine("case JsonTokenType.", tokenType, ":"); - } - - private emitThrow(message: Sourcelike): void { - this.emitLine("throw new Exception(", message, ");"); - } - - private deserializeTypeCode(typeName: Sourcelike): Sourcelike { - switch (typeName) { - case "bool": - return ["reader.GetBoolean()"]; - case "long": - return ["reader.GetInt64()"]; - case "decimal": - return ["reader.GetDecimal()"]; - case "double": - return ["reader.GetDouble()"]; - case "string": - return ["reader.GetString()"]; - default: - return ["JsonSerializer.Deserialize<", typeName, ">(ref reader, options)"]; - } - } - - private serializeValueCode(value: Sourcelike): Sourcelike { - if (value !== "null") return ["JsonSerializer.Serialize(writer, ", value, ", options)"]; - else return ["writer.WriteNullValue()"]; - } - - private emitSerializeClass(): void { - // FIXME: Make Serialize a Named - this.emitType(undefined, AccessModifier.Public, "static class", "Serialize", undefined, () => { - // Sometimes multiple top-levels will resolve to the same type, so we have to take care - // not to emit more than one extension method for the same type. - const seenTypes = new Set(); - this.forEachTopLevel("none", t => { - // FIXME: Make ToJson a Named - if (!seenTypes.has(t)) { - seenTypes.add(t); - this.emitExpressionMember( - ["public static string ToJson(this ", this.topLevelResultType(t), " self)"], - ["JsonSerializer.Serialize(self, ", this._options.namespace, ".Converter.Settings)"] - ); - } - }); - }); - } - - private emitCanConvert(expr: Sourcelike): void { - this.emitExpressionMember("public override bool CanConvert(Type t)", expr); - } - - private emitReadJson(emitBody: () => void, csType: Sourcelike): void { - this.emitLine( - "public override ", - csType, - " Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)" - ); - this.emitBlock(emitBody); - } - - private emitWriteJson(variable: string, emitBody: () => void, csType: Sourcelike): void { - this.emitLine( - "public override void Write(Utf8JsonWriter writer, ", - csType, - " ", - variable, - ", JsonSerializerOptions options)" - ); - this.emitBlock(emitBody); - } - - private converterObject(converterName: Name): Sourcelike { - // FIXME: Get a singleton - return [converterName, ".Singleton"]; - } - - private emitConverterClass(): void { - // FIXME: Make Converter a Named - const converterName: Sourcelike = ["Converter"]; - this.emitType(undefined, AccessModifier.Internal, "static class", converterName, undefined, () => { - // Do not use .Web as defaults. That turns on caseInsensitive property names and will fail the keywords test. - this.emitLine( - "public static readonly JsonSerializerOptions Settings = new(JsonSerializerDefaults.General)" - ); - this.emitBlock(() => { - // this.emitLine("MetadataPropertyHandling = MetadataPropertyHandling.Ignore,"); - // this.emitLine("DateParseHandling = DateParseHandling.None,"); - this.emitLine("Converters ="); - this.emitLine("{"); - this.indent(() => { - for (const [t, converter] of this.typesWithNamedTransformations) { - if (alwaysApplyTransformation(defined(transformationForType(t)))) { - this.emitLine(this.converterObject(converter), ","); - } - } - - this.emitLine("new DateOnlyConverter(),"); - this.emitLine("new TimeOnlyConverter(),"); - this.emitLine("IsoDateTimeOffsetConverter.Singleton"); - // this.emitLine("new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }"); - }); - this.emitLine("},"); - }, true); - }); - } - - private emitDecoderTransformerCase( - tokenCases: string[], - variableName: string, - xfer: Transformer | undefined, - targetType: Type, - emitFinish: (value: Sourcelike) => void - ): void { - if (xfer === undefined) return; - - for (const tokenCase of tokenCases) { - this.emitTokenCase(tokenCase); - } - - this.indent(() => { - const allHandled = this.emitDecodeTransformer(xfer, targetType, emitFinish, variableName); - if (!allHandled) { - this.emitLine("break;"); - } - }); - } - - private emitConsume( - value: Sourcelike, - consumer: Transformer | undefined, - targetType: Type, - emitFinish: (variableName: Sourcelike) => void - ): boolean { - if (consumer === undefined) { - emitFinish(value); - return true; - } else { - return this.emitTransformer(value, consumer, targetType, emitFinish); - } - } - - private emitDecodeTransformer( - xfer: Transformer, - targetType: Type, - emitFinish: (value: Sourcelike) => void, - variableName = "value" - ): boolean { - if (xfer instanceof DecodingTransformer) { - const source = xfer.sourceType; - const converter = this.converterForType(targetType); - if (converter !== undefined) { - const typeSource = this.csType(targetType); - this.emitLine("var converter = ", this.converterObject(converter), ";"); - this.emitLine( - "var ", - variableName, - " = (", - typeSource, - ")converter.ReadJson(reader, typeof(", - typeSource, - "), null, serializer);" - ); - } else if (source.kind !== "null") { - let output = targetType.kind === "double" ? targetType : source; - this.emitLine("var ", variableName, " = ", this.deserializeTypeCode(this.csType(output)), ";"); - } - - return this.emitConsume(variableName, xfer.consumer, targetType, emitFinish); - } else if (xfer instanceof ArrayDecodingTransformer) { - // FIXME: Consume StartArray - if (!(targetType instanceof ArrayType)) { - return panic("Array decoding must produce an array type"); - } - - // FIXME: handle EOF - this.emitLine("reader.Read();"); - this.emitLine("var ", variableName, " = new List<", this.csType(targetType.items), ">();"); - this.emitLine("while (reader.TokenType != JsonToken.EndArray)"); - this.emitBlock(() => { - this.emitDecodeTransformer( - xfer.itemTransformer, - xfer.itemTargetType, - v => this.emitLine(variableName, ".Add(", v, ");"), - "arrayItem" - ); - // FIXME: handle EOF - this.emitLine("reader.Read();"); - }); - let result: Sourcelike = variableName; - if (!this._options.useList) { - result = [result, ".ToArray()"]; - } - - emitFinish(result); - return true; - } else if (xfer instanceof DecodingChoiceTransformer) { - this.emitDecoderSwitch(() => { - const nullTransformer = xfer.nullTransformer; - if (nullTransformer !== undefined) { - this.emitTokenCase("Null"); - this.indent(() => { - const allHandled = this.emitDecodeTransformer(nullTransformer, targetType, emitFinish, "null"); - if (!allHandled) { - this.emitLine("break"); - } - }); - } - - this.emitDecoderTransformerCase( - ["Number"], - "integerValue", - xfer.integerTransformer, - targetType, - emitFinish - ); - this.emitDecoderTransformerCase( - ["Number"], - // xfer.integerTransformer === undefined ? ["Integer", "Float"] : ["Float"], - "doubleValue", - xfer.doubleTransformer, - targetType, - emitFinish - ); - this.emitDecoderTransformerCase( - ["True", "False"], - "boolValue", - xfer.boolTransformer, - targetType, - emitFinish - ); - this.emitDecoderTransformerCase( - // ["String", "Date"], - ["String"], - "stringValue", - xfer.stringTransformer, - targetType, - emitFinish - ); - this.emitDecoderTransformerCase( - ["StartObject"], - "objectValue", - xfer.objectTransformer, - targetType, - emitFinish - ); - this.emitDecoderTransformerCase( - ["StartArray"], - "arrayValue", - xfer.arrayTransformer, - targetType, - emitFinish - ); - }); - return false; - } else { - return panic("Unknown transformer"); - } - } - - private stringCaseValue(t: Type, stringCase: string): Sourcelike { - if (t.kind === "string") { - return ['"', utf16StringEscape(stringCase), '"']; - } else if (t instanceof EnumType) { - return [this.nameForNamedType(t), ".", this.nameForEnumCase(t, stringCase)]; - } - - return panic(`Type ${t.kind} does not have string cases`); - } - - private emitTransformer( - variable: Sourcelike, - xfer: Transformer, - targetType: Type, - emitFinish: (value: Sourcelike) => void - ): boolean { - function directTargetType(continuation: Transformer | undefined): Type { - if (continuation === undefined) { - return targetType; - } - - return followTargetType(continuation.sourceType); - } - - if (xfer instanceof ChoiceTransformer) { - const caseXfers = xfer.transformers; - if (caseXfers.length > 1 && caseXfers.every(caseXfer => caseXfer instanceof StringMatchTransformer)) { - this.emitLine("switch (", variable, ")"); - this.emitBlock(() => { - for (const caseXfer of caseXfers) { - const matchXfer = caseXfer as StringMatchTransformer; - const value = this.stringCaseValue( - followTargetType(matchXfer.sourceType), - matchXfer.stringCase - ); - this.emitLine("case ", value, ":"); - this.indent(() => { - const allDone = this.emitTransformer( - variable, - matchXfer.transformer, - targetType, - emitFinish - ); - if (!allDone) { - this.emitLine("break;"); - } - }); - } - }); - // FIXME: Can we check for exhaustiveness? For enums it should be easy. - return false; - } else { - for (const caseXfer of caseXfers) { - this.emitTransformer(variable, caseXfer, targetType, emitFinish); - } - } - } else if (xfer instanceof UnionMemberMatchTransformer) { - const memberType = xfer.memberType; - const maybeNullable = nullableFromUnion(xfer.sourceType); - let test: Sourcelike; - let member: Sourcelike; - if (maybeNullable !== null) { - if (memberType.kind === "null") { - test = [variable, " == null"]; - member = "null"; - } else { - test = [variable, " != null"]; - member = variable; - } - } else if (memberType.kind === "null") { - test = [variable, ".IsNull"]; - member = "null"; - } else { - const memberName = this.nameForUnionMember(xfer.sourceType, memberType); - member = [variable, ".", memberName]; - test = [member, " != null"]; - } - - if (memberType.kind !== "null" && isValueType(memberType)) { - member = [member, ".Value"]; - } - - this.emitLine("if (", test, ")"); - this.emitBlock(() => this.emitTransformer(member, xfer.transformer, targetType, emitFinish)); - } else if (xfer instanceof StringMatchTransformer) { - const value = this.stringCaseValue(followTargetType(xfer.sourceType), xfer.stringCase); - this.emitLine("if (", variable, " == ", value, ")"); - this.emitBlock(() => this.emitTransformer(variable, xfer.transformer, targetType, emitFinish)); - } else if (xfer instanceof EncodingTransformer) { - const converter = this.converterForType(xfer.sourceType); - if (converter !== undefined) { - this.emitLine("var converter = ", this.converterObject(converter), ";"); - this.emitLine("converter.WriteJson(writer, ", variable, ", serializer);"); - } else { - this.emitLine(this.serializeValueCode(variable), ";"); - } - - emitFinish([]); - return true; - } else if (xfer instanceof ArrayEncodingTransformer) { - this.emitLine("writer.WriteStartArray();"); - const itemVariable = "arrayItem"; - this.emitLine("foreach (var ", itemVariable, " in ", variable, ")"); - this.emitBlock(() => { - this.emitTransformer(itemVariable, xfer.itemTransformer, xfer.itemTargetType, () => { - return; - }); - }); - this.emitLine("writer.WriteEndArray();"); - emitFinish([]); - return true; - } else if (xfer instanceof ParseStringTransformer) { - const immediateTargetType = xfer.consumer === undefined ? targetType : xfer.consumer.sourceType; - switch (immediateTargetType.kind) { - case "date-time": - this.emitLine("DateTimeOffset dt;"); - this.emitLine("if (DateTimeOffset.TryParse(", variable, ", out dt))"); - this.emitBlock(() => this.emitConsume("dt", xfer.consumer, targetType, emitFinish)); - break; - case "uuid": - this.emitLine("Guid guid;"); - this.emitLine("if (Guid.TryParse(", variable, ", out guid))"); - this.emitBlock(() => this.emitConsume("guid", xfer.consumer, targetType, emitFinish)); - break; - case "uri": - this.emitLine("try"); - this.emitBlock(() => { - // this.emitLine("var uri = new Uri(", variable, ");"); - // The default value about:blank should never happen, but this way we avoid a null reference warning. - this.emitLine('var uri = new Uri("about:blank");'); - this.emitLine("if (!string.IsNullOrEmpty(stringValue))"); - this.emitBlock(() => { - this.emitLine("uri = new Uri(", variable, ");"); - }); - this.emitConsume("uri", xfer.consumer, targetType, emitFinish); - }); - this.emitLine("catch (UriFormatException) {}"); - break; - case "integer": - this.emitLine("long l;"); - this.emitLine("if (Int64.TryParse(", variable, ", out l))"); - this.emitBlock(() => this.emitConsume("l", xfer.consumer, targetType, emitFinish)); - break; - case "bool": - this.emitLine("bool b;"); - this.emitLine("if (Boolean.TryParse(", variable, ", out b))"); - this.emitBlock(() => this.emitConsume("b", xfer.consumer, targetType, emitFinish)); - break; - default: - return panic(`Parsing string to ${immediateTargetType.kind} not supported`); - } - } else if (xfer instanceof StringifyTransformer) { - switch (xfer.sourceType.kind) { - case "date-time": - return this.emitConsume( - [variable, '.ToString("o", System.Globalization.CultureInfo.InvariantCulture)'], - xfer.consumer, - targetType, - emitFinish - ); - case "uuid": - return this.emitConsume( - [variable, '.ToString("D", System.Globalization.CultureInfo.InvariantCulture)'], - xfer.consumer, - targetType, - emitFinish - ); - case "integer": - case "uri": - return this.emitConsume([variable, ".ToString()"], xfer.consumer, targetType, emitFinish); - case "bool": - this.emitLine("var boolString = ", variable, ' ? "true" : "false";'); - return this.emitConsume("boolString", xfer.consumer, targetType, emitFinish); - default: - return panic(`Stringifying ${xfer.sourceType.kind} not supported`); - } - } else if (xfer instanceof StringProducerTransformer) { - const value = this.stringCaseValue(directTargetType(xfer.consumer), xfer.result); - return this.emitConsume(value, xfer.consumer, targetType, emitFinish); - } else if (xfer instanceof MinMaxLengthCheckTransformer) { - const min = xfer.minLength; - const max = xfer.maxLength; - const conditions: Sourcelike[] = []; - - if (min !== undefined) { - conditions.push([variable, ".Length >= ", min.toString()]); - } - - if (max !== undefined) { - conditions.push([variable, ".Length <= ", max.toString()]); - } - - this.emitLine("if (", arrayIntercalate([" && "], conditions), ")"); - this.emitBlock(() => this.emitConsume(variable, xfer.consumer, targetType, emitFinish)); - return false; - } else if (xfer instanceof MinMaxValueTransformer) { - const min = xfer.minimum; - const max = xfer.maximum; - const conditions: Sourcelike[] = []; - - if (min !== undefined) { - conditions.push([variable, " >= ", min.toString()]); - } - - if (max !== undefined) { - conditions.push([variable, " <= ", max.toString()]); - } - - this.emitLine("if (", arrayIntercalate([" && "], conditions), ")"); - this.emitBlock(() => this.emitConsume(variable, xfer.consumer, targetType, emitFinish)); - return false; - } else if (xfer instanceof UnionInstantiationTransformer) { - if (!(targetType instanceof UnionType)) { - return panic("Union instantiation transformer must produce a union type"); - } - - const maybeNullable = nullableFromUnion(targetType); - if (maybeNullable !== null) { - emitFinish(variable); - } else { - const unionName = this.nameForNamedType(targetType); - let initializer: Sourcelike; - if (xfer.sourceType.kind === "null") { - initializer = " "; - } else { - const memberName = this.nameForUnionMember(targetType, xfer.sourceType); - initializer = [" ", memberName, " = ", variable, " "]; - } - - emitFinish(["new ", unionName, " {", initializer, "}"]); - } - - return true; - } else { - return panic("Unknown transformer"); - } - - return false; - } - - private emitTransformation(converterName: Name, t: Type): void { - const xf = defined(transformationForType(t)); - const reverse = xf.reverse; - const targetType = xf.targetType; - const xfer = xf.transformer; - const csType = this.csType(targetType); - // const haveNullable = isValueType(targetType); - - // if (haveNullable) { - // converterName = ['Nullable', converterName]; - // csType = [csType, "?"]; - // } - this.emitType( - undefined, - AccessModifier.Internal, - "class", - converterName, - ["JsonConverter<", csType, ">"], - () => { - let canConvertExpr: Sourcelike = ["t == typeof(", csType, ")"]; - this.emitCanConvert(canConvertExpr); - this.ensureBlankLine(); - this.emitReadJson(() => { - // FIXME: It's unsatisfying that we need this. The reason is that we not - // only match T, but also T?. If we didn't, then the T in T? would not be - // deserialized with our converter but with the default one. Can we check - // whether the type is a nullable? - // FIXME: This could duplicate one of the cases handled below in - // `emitDecodeTransformer`. - // if (haveNullable && !(targetType instanceof UnionType)) { - // this.emitLine("if (reader.TokenType == JsonTokenType.Null) return null;"); - // } - - const allHandled = this.emitDecodeTransformer(xfer, targetType, v => - this.emitLine("return ", v, ";") - ); - if (!allHandled) { - this.emitThrow(['"Cannot unmarshal type ', csType, '"']); - } - }, csType); - this.ensureBlankLine(); - this.emitWriteJson( - "value", - () => { - // FIXME: See above. - // if (haveNullable && !(targetType instanceof UnionType)) { - // this.emitLine("if (value == null)"); - // this.emitBlock(() => { - // this.emitLine("writer.WriteNullValue();"); - // this.emitLine("return;"); - // }); - // } - - const allHandled = this.emitTransformer("value", reverse.transformer, reverse.targetType, () => - this.emitLine("return;") - ); - if (!allHandled) { - this.emitThrow(['"Cannot marshal type ', csType, '"']); - } - }, - csType - ); - this.ensureBlankLine(); - this.emitLine("public static readonly ", converterName, " Singleton = new ", converterName, "();"); - } - ); - } - - protected emitRequiredHelpers(): void { - if (this._needHelpers) { - this.forEachTopLevel("leading-and-interposing", (t, n) => this.emitFromJsonForTopLevel(t, n)); - this.ensureBlankLine(); - this.emitSerializeClass(); - } - - if (this._needHelpers || (this._needAttributes && (this.haveNamedUnions || this.haveEnums))) { - this.ensureBlankLine(); - this.emitConverterClass(); - this.forEachTransformation("leading-and-interposing", (n, t) => this.emitTransformation(n, t)); - this.emitMultiline(` -public class DateOnlyConverter : JsonConverter -{ - private readonly string serializationFormat; - public DateOnlyConverter() : this(null) { } - - public DateOnlyConverter(string? serializationFormat) - { - this.serializationFormat = serializationFormat ?? "yyyy-MM-dd"; - } - - public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var value = reader.GetString(); - return DateOnly.Parse(value!); - } - - public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options) - => writer.WriteStringValue(value.ToString(serializationFormat)); -} - -public class TimeOnlyConverter : JsonConverter -{ - private readonly string serializationFormat; - - public TimeOnlyConverter() : this(null) { } - - public TimeOnlyConverter(string? serializationFormat) - { - this.serializationFormat = serializationFormat ?? "HH:mm:ss.fff"; - } - - public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var value = reader.GetString(); - return TimeOnly.Parse(value!); - } - - public override void Write(Utf8JsonWriter writer, TimeOnly value, JsonSerializerOptions options) - => writer.WriteStringValue(value.ToString(serializationFormat)); -} - -internal class IsoDateTimeOffsetConverter : JsonConverter -{ - public override bool CanConvert(Type t) => t == typeof(DateTimeOffset); - - private const string DefaultDateTimeFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFFK"; - - private DateTimeStyles _dateTimeStyles = DateTimeStyles.RoundtripKind; - private string? _dateTimeFormat; - private CultureInfo? _culture; - - public DateTimeStyles DateTimeStyles - { - get => _dateTimeStyles; - set => _dateTimeStyles = value; - } - - public string? DateTimeFormat - { - get => _dateTimeFormat ?? string.Empty; - set => _dateTimeFormat = (string.IsNullOrEmpty(value)) ? null : value; - } - - public CultureInfo Culture - { - get => _culture ?? CultureInfo.CurrentCulture; - set => _culture = value; - } - - public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) - { - string text; - - - if ((_dateTimeStyles & DateTimeStyles.AdjustToUniversal) == DateTimeStyles.AdjustToUniversal - || (_dateTimeStyles & DateTimeStyles.AssumeUniversal) == DateTimeStyles.AssumeUniversal) - { - value = value.ToUniversalTime(); - } - - text = value.ToString(_dateTimeFormat ?? DefaultDateTimeFormat, Culture); - - writer.WriteStringValue(text); - } - - public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - string? dateText = reader.GetString(); - - if (string.IsNullOrEmpty(dateText) == false) - { - if (!string.IsNullOrEmpty(_dateTimeFormat)) - { - return DateTimeOffset.ParseExact(dateText, _dateTimeFormat, Culture, _dateTimeStyles); - } - else - { - return DateTimeOffset.Parse(dateText, Culture, _dateTimeStyles); - } - } - else - { - return default(DateTimeOffset); - } - } - - - public static readonly IsoDateTimeOffsetConverter Singleton = new IsoDateTimeOffsetConverter(); -}`); - } - } - - protected needNamespace(): boolean { - return this._needNamespaces; - } -} diff --git a/packages/quicktype-core/src/language/CSharp/CSharpRenderer.ts b/packages/quicktype-core/src/language/CSharp/CSharpRenderer.ts new file mode 100644 index 000000000..09cfcc3ca --- /dev/null +++ b/packages/quicktype-core/src/language/CSharp/CSharpRenderer.ts @@ -0,0 +1,390 @@ +import { arrayIntercalate } from "collection-utils"; + +import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../../Annotation"; +import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../../ConvenienceRenderer"; +import { type Name, type Namer } from "../../Naming"; +import { type RenderContext } from "../../Renderer"; +import { type OptionValues } from "../../RendererOptions"; +import { type Sourcelike, maybeAnnotated } from "../../Source"; +import { assert } from "../../support/Support"; +import { type TargetLanguage } from "../../TargetLanguage"; +import { followTargetType } from "../../Transformers"; +import { type ClassProperty, type ClassType, type EnumType, type Type, type UnionType } from "../../Type"; +import { directlyReachableSingleNamedType, matchType, nullableFromUnion, removeNullFromUnion } from "../../TypeUtils"; + +import { type cSharpOptions } from "./language"; +import { + AccessModifier, + csTypeForTransformedStringType, + isValueType, + namingFunction, + namingFunctionKeep, + noFollow +} from "./utils"; + +export class CSharpRenderer extends ConvenienceRenderer { + public constructor( + targetLanguage: TargetLanguage, + renderContext: RenderContext, + private readonly _csOptions: OptionValues + ) { + super(targetLanguage, renderContext); + } + + protected forbiddenNamesForGlobalNamespace(): string[] { + return ["QuickType", "Type", "System", "Console", "Exception", "DateTimeOffset", "Guid", "Uri"]; + } + + protected forbiddenForObjectProperties(_: ClassType, classNamed: Name): ForbiddenWordsInfo { + return { + names: [ + classNamed, + "ToString", + "GetHashCode", + "Finalize", + "Equals", + "GetType", + "MemberwiseClone", + "ReferenceEquals" + ], + includeGlobalForbidden: false + }; + } + + protected forbiddenForUnionMembers(_: UnionType, unionNamed: Name): ForbiddenWordsInfo { + return { names: [unionNamed], includeGlobalForbidden: true }; + } + + protected makeNamedTypeNamer(): Namer { + return namingFunction; + } + + protected namerForObjectProperty(): Namer { + return this._csOptions.keepPropertyName ? namingFunctionKeep : namingFunction; + } + + protected makeUnionMemberNamer(): Namer { + return namingFunction; + } + + protected makeEnumCaseNamer(): Namer { + return namingFunction; + } + + protected unionNeedsName(u: UnionType): boolean { + return nullableFromUnion(u) === null; + } + + protected namedTypeToNameForTopLevel(type: Type): Type | undefined { + // If the top-level type doesn't contain any classes or unions + // we have to define a class just for the `FromJson` method, in + // emitFromJsonForTopLevel. + return directlyReachableSingleNamedType(type); + } + + protected emitBlock(f: () => void, semicolon = false): void { + this.emitLine("{"); + this.indent(f); + this.emitLine("}", semicolon ? ";" : ""); + } + + protected get doubleType(): string { + return this._csOptions.useDecimal ? "decimal" : "double"; + } + + protected csType(t: Type, follow: (t: Type) => Type = followTargetType, withIssues = false): Sourcelike { + const actualType = follow(t); + return matchType( + actualType, + _anyType => maybeAnnotated(withIssues, anyTypeIssueAnnotation, this._csOptions.typeForAny), + _nullType => maybeAnnotated(withIssues, nullTypeIssueAnnotation, this._csOptions.typeForAny), + _boolType => "bool", + _integerType => "long", + _doubleType => this.doubleType, + _stringType => "string", + arrayType => { + const itemsType = this.csType(arrayType.items, follow, withIssues); + if (this._csOptions.useList) { + return ["List<", itemsType, ">"]; + } else { + return [itemsType, "[]"]; + } + }, + classType => this.nameForNamedType(classType), + mapType => ["Dictionary"], + enumType => this.nameForNamedType(enumType), + unionType => { + const nullable = nullableFromUnion(unionType); + if (nullable !== null) return this.nullableCSType(nullable, noFollow); + return this.nameForNamedType(unionType); + }, + transformedStringType => csTypeForTransformedStringType(transformedStringType) + ); + } + + protected nullableCSType(t: Type, follow: (t: Type) => Type = followTargetType, withIssues = false): Sourcelike { + t = followTargetType(t); + const csType = this.csType(t, follow, withIssues); + if (isValueType(t)) { + return [csType, "?"]; + } else { + return csType; + } + } + + protected baseclassForType(_t: Type): Sourcelike | undefined { + return undefined; + } + + protected emitType( + description: string[] | undefined, + accessModifier: AccessModifier, + declaration: Sourcelike, + name: Sourcelike, + baseclass: Sourcelike | undefined, + emitter: () => void + ): void { + switch (accessModifier) { + case AccessModifier.Public: + declaration = ["public ", declaration]; + break; + case AccessModifier.Internal: + declaration = ["internal ", declaration]; + break; + default: + break; + } + + this.emitDescription(description); + if (baseclass === undefined) { + this.emitLine(declaration, " ", name); + } else { + this.emitLine(declaration, " ", name, " : ", baseclass); + } + + this.emitBlock(emitter); + } + + protected attributesForProperty( + _property: ClassProperty, + _name: Name, + _c: ClassType, + _jsonName: string + ): Sourcelike[] | undefined { + return undefined; + } + + protected propertyDefinition(property: ClassProperty, name: Name, _c: ClassType, _jsonName: string): Sourcelike { + const t = property.type; + const csType = property.isOptional + ? this.nullableCSType(t, followTargetType, true) + : this.csType(t, followTargetType, true); + + const propertyArray = ["public "]; + + if (this._csOptions.virtual) propertyArray.push("virtual "); + + return [...propertyArray, csType, " ", name, " { get; set; }"]; + } + + protected emitDescriptionBlock(lines: Sourcelike[]): void { + const start = "/// "; + if (this._csOptions.dense) { + this.emitLine(start, lines.join("; "), ""); + } else { + this.emitCommentLines(lines, { lineStart: "/// ", beforeComment: start, afterComment: "/// " }); + } + } + + protected blankLinesBetweenAttributes(): boolean { + return false; + } + + private emitClassDefinition(c: ClassType, className: Name): void { + this.emitType( + this.descriptionForType(c), + AccessModifier.Public, + "partial class", + className, + this.baseclassForType(c), + () => { + if (c.getProperties().size === 0) return; + const blankLines = this.blankLinesBetweenAttributes() ? "interposing" : "none"; + let columns: Sourcelike[][] = []; + let isFirstProperty = true; + let previousDescription: string[] | undefined = undefined; + this.forEachClassProperty(c, blankLines, (name, jsonName, p) => { + const attributes = this.attributesForProperty(p, name, c, jsonName); + const description = this.descriptionForClassProperty(c, jsonName); + const property = this.propertyDefinition(p, name, c, jsonName); + if (attributes === undefined) { + if ( + // Descriptions should be preceded by an empty line + (!isFirstProperty && description !== undefined) || + // If the previous property has a description, leave an empty line + previousDescription !== undefined + ) { + this.ensureBlankLine(); + } + + this.emitDescription(description); + this.emitLine(property); + } else if (this._csOptions.dense && attributes.length > 0) { + const comment = description === undefined ? "" : ` // ${description.join("; ")}`; + columns.push([attributes, " ", property, comment]); + } else { + this.emitDescription(description); + for (const attribute of attributes) { + this.emitLine(attribute); + } + + this.emitLine(property); + } + + isFirstProperty = false; + previousDescription = description; + }); + if (columns.length > 0) { + this.emitTable(columns); + } + } + ); + } + + private emitUnionDefinition(u: UnionType, unionName: Name): void { + const nonNulls = removeNullFromUnion(u, true)[1]; + this.emitType( + this.descriptionForType(u), + AccessModifier.Public, + "partial struct", + unionName, + this.baseclassForType(u), + () => { + this.forEachUnionMember(u, nonNulls, "none", null, (fieldName, t) => { + const csType = this.nullableCSType(t); + this.emitLine("public ", csType, " ", fieldName, ";"); + }); + this.ensureBlankLine(); + const nullTests: Sourcelike[] = Array.from(nonNulls).map(t => [ + this.nameForUnionMember(u, t), + " == null" + ]); + this.ensureBlankLine(); + this.forEachUnionMember(u, nonNulls, "none", null, (fieldName, t) => { + const csType = this.csType(t); + this.emitExpressionMember( + ["public static implicit operator ", unionName, "(", csType, " ", fieldName, ")"], + ["new ", unionName, " { ", fieldName, " = ", fieldName, " }"] + ); + }); + if (u.findMember("null") === undefined) return; + this.emitExpressionMember("public bool IsNull", arrayIntercalate(" && ", nullTests), true); + } + ); + } + + private emitEnumDefinition(e: EnumType, enumName: Name): void { + const caseNames: Sourcelike[] = []; + this.forEachEnumCase(e, "none", name => { + if (caseNames.length > 0) caseNames.push(", "); + caseNames.push(name); + }); + this.emitDescription(this.descriptionForType(e)); + this.emitLine("public enum ", enumName, " { ", caseNames, " };"); + } + + protected emitExpressionMember(declare: Sourcelike, define: Sourcelike, isProperty = false): void { + if (this._csOptions.version === 5) { + this.emitLine(declare); + this.emitBlock(() => { + const stmt = ["return ", define, ";"]; + if (isProperty) { + this.emitLine("get"); + this.emitBlock(() => this.emitLine(stmt)); + } else { + this.emitLine(stmt); + } + }); + } else { + this.emitLine(declare, " => ", define, ";"); + } + } + + protected emitTypeSwitch( + types: Iterable, + condition: (t: T) => Sourcelike, + withBlock: boolean, + withReturn: boolean, + f: (t: T) => void + ): void { + assert(!withReturn || withBlock, "Can only have return with block"); + for (const t of types) { + this.emitLine("if (", condition(t), ")"); + if (withBlock) { + this.emitBlock(() => { + f(t); + if (withReturn) { + this.emitLine("return;"); + } + }); + } else { + this.indent(() => f(t)); + } + } + } + + protected emitUsing(ns: Sourcelike): void { + this.emitLine("using ", ns, ";"); + } + + protected emitUsings(): void { + for (const ns of ["System", "System.Collections.Generic"]) { + this.emitUsing(ns); + } + } + + protected emitRequiredHelpers(): void { + return; + } + + private emitTypesAndSupport(): void { + this.forEachObject("leading-and-interposing", (c: ClassType, name: Name) => this.emitClassDefinition(c, name)); + this.forEachEnum("leading-and-interposing", (e, name) => this.emitEnumDefinition(e, name)); + this.forEachUnion("leading-and-interposing", (u, name) => this.emitUnionDefinition(u, name)); + this.emitRequiredHelpers(); + } + + protected emitDefaultLeadingComments(): void { + return; + } + + protected emitDefaultFollowingComments(): void { + return; + } + + protected needNamespace(): boolean { + return true; + } + + protected emitSourceStructure(): void { + if (this.leadingComments !== undefined) { + this.emitComments(this.leadingComments); + } else { + this.emitDefaultLeadingComments(); + } + + this.ensureBlankLine(); + if (this.needNamespace()) { + this.emitLine("namespace ", this._csOptions.namespace); + this.emitBlock(() => { + this.emitUsings(); + this.emitTypesAndSupport(); + }); + } else { + this.emitUsings(); + this.emitTypesAndSupport(); + } + + this.emitDefaultFollowingComments(); + } +} diff --git a/packages/quicktype-core/src/language/CSharp/NewtonSoftCSharpRenderer.ts b/packages/quicktype-core/src/language/CSharp/NewtonSoftCSharpRenderer.ts new file mode 100644 index 000000000..4d6b09da6 --- /dev/null +++ b/packages/quicktype-core/src/language/CSharp/NewtonSoftCSharpRenderer.ts @@ -0,0 +1,803 @@ +import { arrayIntercalate } from "collection-utils"; + +import { type ForbiddenWordsInfo, inferredNameOrder } from "../../ConvenienceRenderer"; +import { DependencyName, type Name, SimpleName } from "../../Naming"; +import { type RenderContext } from "../../Renderer"; +import { type OptionValues } from "../../RendererOptions"; +import { type Sourcelike, modifySource } from "../../Source"; +import { camelCase, utf16StringEscape } from "../../support/Strings"; +import { defined, panic } from "../../support/Support"; +import { type TargetLanguage } from "../../TargetLanguage"; +import { + ArrayDecodingTransformer, + ArrayEncodingTransformer, + ChoiceTransformer, + DecodingChoiceTransformer, + DecodingTransformer, + EncodingTransformer, + MinMaxLengthCheckTransformer, + MinMaxValueTransformer, + ParseStringTransformer, + StringMatchTransformer, + StringProducerTransformer, + StringifyTransformer, + type Transformation, + type Transformer, + UnionInstantiationTransformer, + UnionMemberMatchTransformer, + followTargetType, + transformationForType +} from "../../Transformers"; +import { ArrayType, type ClassProperty, ClassType, EnumType, type Type, UnionType } from "../../Type"; +import { nullableFromUnion } from "../../TypeUtils"; + +import { CSharpRenderer } from "./CSharpRenderer"; +import { type newtonsoftCSharpOptions } from "./language"; +import { + AccessModifier, + alwaysApplyTransformation, + denseJsonPropertyName, + denseNullValueHandlingEnumName, + denseRequiredEnumName, + isValueType, + namingFunction +} from "./utils"; + +export class NewtonsoftCSharpRenderer extends CSharpRenderer { + private readonly _enumExtensionsNames = new Map(); + + private readonly _needHelpers: boolean; + + private readonly _needAttributes: boolean; + + private readonly _needNamespaces: boolean; + + public constructor( + targetLanguage: TargetLanguage, + renderContext: RenderContext, + private readonly _options: OptionValues + ) { + super(targetLanguage, renderContext, _options); + this._needHelpers = _options.features.helpers; + this._needAttributes = _options.features.attributes; + this._needNamespaces = _options.features.namespaces; + } + + protected forbiddenNamesForGlobalNamespace(): string[] { + const forbidden = [ + "Converter", + "JsonConverter", + "JsonSerializer", + "JsonWriter", + "JsonToken", + "Serialize", + "Newtonsoft", + "MetadataPropertyHandling", + "DateParseHandling", + "FromJson", + "Required" + ]; + if (this._options.dense) { + forbidden.push("J", "R", "N"); + } + + if (this._options.baseclass !== undefined) { + forbidden.push(this._options.baseclass); + } + + return super.forbiddenNamesForGlobalNamespace().concat(forbidden); + } + + protected forbiddenForObjectProperties(c: ClassType, className: Name): ForbiddenWordsInfo { + const result = super.forbiddenForObjectProperties(c, className); + result.names = result.names.concat(["ToJson", "FromJson", "Required"]); + return result; + } + + protected makeNameForTransformation(xf: Transformation, typeName: Name | undefined): Name { + if (typeName === undefined) { + let xfer = xf.transformer; + if (xfer instanceof DecodingTransformer && xfer.consumer !== undefined) { + xfer = xfer.consumer; + } + + return new SimpleName([`${xfer.kind}_converter`], namingFunction, inferredNameOrder + 30); + } + + return new DependencyName(namingFunction, typeName.order + 30, lookup => `${lookup(typeName)}_converter`); + } + + protected makeNamedTypeDependencyNames(t: Type, name: Name): DependencyName[] { + if (!(t instanceof EnumType)) return []; + + const extensionsName = new DependencyName( + namingFunction, + name.order + 30, + lookup => `${lookup(name)}_extensions` + ); + this._enumExtensionsNames.set(name, extensionsName); + return [extensionsName]; + } + + protected emitUsings(): void { + // FIXME: We need System.Collections.Generic whenever we have maps or use List. + if (!this._needAttributes && !this._needHelpers) return; + + super.emitUsings(); + this.ensureBlankLine(); + + for (const ns of ["System.Globalization", "Newtonsoft.Json", "Newtonsoft.Json.Converters"]) { + this.emitUsing(ns); + } + + if (this._options.dense) { + this.emitUsing([denseJsonPropertyName, " = Newtonsoft.Json.JsonPropertyAttribute"]); + this.emitUsing([denseRequiredEnumName, " = Newtonsoft.Json.Required"]); + this.emitUsing([denseNullValueHandlingEnumName, " = Newtonsoft.Json.NullValueHandling"]); + } + + if (this._options.baseclass === "EntityData") { + this.emitUsing("Microsoft.Azure.Mobile.Server"); + } + } + + protected baseclassForType(_t: Type): Sourcelike | undefined { + return this._options.baseclass; + } + + protected emitDefaultLeadingComments(): void { + if (!this._needHelpers) return; + + this.emitLine("// "); + this.emitLine("//"); + this.emitLine( + "// To parse this JSON data, add NuGet 'Newtonsoft.Json' then do", + this.topLevels.size === 1 ? "" : " one of these", + ":" + ); + this.emitLine("//"); + this.emitLine("// using ", this._options.namespace, ";"); + this.emitLine("//"); + this.forEachTopLevel("none", (t, topLevelName) => { + let rhs: Sourcelike; + if (t instanceof EnumType) { + rhs = ["JsonConvert.DeserializeObject<", topLevelName, ">(jsonString)"]; + } else { + rhs = [topLevelName, ".FromJson(jsonString)"]; + } + + this.emitLine("// var ", modifySource(camelCase, topLevelName), " = ", rhs, ";"); + }); + } + + private converterForType(t: Type): Name | undefined { + let xf = transformationForType(t); + + if (xf === undefined && t instanceof UnionType) { + const maybeNullable = nullableFromUnion(t); + if (maybeNullable !== null) { + t = maybeNullable; + xf = transformationForType(t); + } + } + + if (xf === undefined) return undefined; + + if (alwaysApplyTransformation(xf)) return undefined; + + return defined(this.nameForTransformation(t)); + } + + protected attributesForProperty( + property: ClassProperty, + _name: Name, + _c: ClassType, + jsonName: string + ): Sourcelike[] | undefined { + if (!this._needAttributes) return undefined; + + const attributes: Sourcelike[] = []; + + const jsonProperty = this._options.dense ? denseJsonPropertyName : "JsonProperty"; + const escapedName = utf16StringEscape(jsonName); + const isNullable = followTargetType(property.type).isNullable; + const isOptional = property.isOptional; + const requiredClass = this._options.dense ? "R" : "Required"; + const nullValueHandlingClass = this._options.dense ? "N" : "NullValueHandling"; + const nullValueHandling = + isOptional && !isNullable ? [", NullValueHandling = ", nullValueHandlingClass, ".Ignore"] : []; + let required: Sourcelike; + if (!this._options.checkRequired || (isOptional && isNullable)) { + required = [nullValueHandling]; + } else if (isOptional && !isNullable) { + required = [", Required = ", requiredClass, ".DisallowNull", nullValueHandling]; + } else if (!isOptional && isNullable) { + required = [", Required = ", requiredClass, ".AllowNull"]; + } else { + required = [", Required = ", requiredClass, ".Always", nullValueHandling]; + } + + attributes.push(["[", jsonProperty, '("', escapedName, '"', required, ")]"]); + + const converter = this.converterForType(property.type); + if (converter !== undefined) { + attributes.push(["[JsonConverter(typeof(", converter, "))]"]); + } + + return attributes; + } + + protected blankLinesBetweenAttributes(): boolean { + return this._needAttributes && !this._options.dense; + } + + // The "this" type can't be `dynamic`, so we have to force it to `object`. + private topLevelResultType(t: Type): Sourcelike { + return t.kind === "any" || t.kind === "none" ? "object" : this.csType(t); + } + + private emitFromJsonForTopLevel(t: Type, name: Name): void { + if (t instanceof EnumType) return; + + let partial: string; + let typeKind: string; + const definedType = this.namedTypeToNameForTopLevel(t); + if (definedType !== undefined) { + partial = "partial "; + typeKind = definedType instanceof ClassType ? "class" : "struct"; + } else { + partial = ""; + typeKind = "class"; + } + + const csType = this.topLevelResultType(t); + this.emitType(undefined, AccessModifier.Public, [partial, typeKind], name, this.baseclassForType(t), () => { + // FIXME: Make FromJson a Named + this.emitExpressionMember( + ["public static ", csType, " FromJson(string json)"], + ["JsonConvert.DeserializeObject<", csType, ">(json, ", this._options.namespace, ".Converter.Settings)"] + ); + }); + } + + private emitDecoderSwitch(emitBody: () => void): void { + this.emitLine("switch (reader.TokenType)"); + this.emitBlock(emitBody); + } + + private emitTokenCase(tokenType: string): void { + this.emitLine("case JsonToken.", tokenType, ":"); + } + + private emitThrow(message: Sourcelike): void { + this.emitLine("throw new Exception(", message, ");"); + } + + private deserializeTypeCode(typeName: Sourcelike): Sourcelike { + return ["serializer.Deserialize<", typeName, ">(reader)"]; + } + + private serializeValueCode(value: Sourcelike): Sourcelike { + return ["serializer.Serialize(writer, ", value, ")"]; + } + + private emitSerializeClass(): void { + // FIXME: Make Serialize a Named + this.emitType(undefined, AccessModifier.Public, "static class", "Serialize", undefined, () => { + // Sometimes multiple top-levels will resolve to the same type, so we have to take care + // not to emit more than one extension method for the same type. + const seenTypes = new Set(); + this.forEachTopLevel("none", t => { + // FIXME: Make ToJson a Named + if (!seenTypes.has(t)) { + seenTypes.add(t); + this.emitExpressionMember( + ["public static string ToJson(this ", this.topLevelResultType(t), " self)"], + ["JsonConvert.SerializeObject(self, ", this._options.namespace, ".Converter.Settings)"] + ); + } + }); + }); + } + + private emitCanConvert(expr: Sourcelike): void { + this.emitExpressionMember("public override bool CanConvert(Type t)", expr); + } + + private emitReadJson(emitBody: () => void): void { + this.emitLine( + "public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer)" + ); + this.emitBlock(emitBody); + } + + private emitWriteJson(variable: string, emitBody: () => void): void { + this.emitLine( + "public override void WriteJson(JsonWriter writer, object ", + variable, + ", JsonSerializer serializer)" + ); + this.emitBlock(emitBody); + } + + private converterObject(converterName: Name): Sourcelike { + // FIXME: Get a singleton + return [converterName, ".Singleton"]; + } + + private emitConverterClass(): void { + // FIXME: Make Converter a Named + const converterName: Sourcelike = ["Converter"]; + this.emitType(undefined, AccessModifier.Internal, "static class", converterName, undefined, () => { + this.emitLine("public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings"); + this.emitBlock(() => { + this.emitLine("MetadataPropertyHandling = MetadataPropertyHandling.Ignore,"); + this.emitLine("DateParseHandling = DateParseHandling.None,"); + this.emitLine("Converters ="); + this.emitLine("{"); + this.indent(() => { + for (const [t, converter] of this.typesWithNamedTransformations) { + if (alwaysApplyTransformation(defined(transformationForType(t)))) { + this.emitLine(this.converterObject(converter), ","); + } + } + + this.emitLine("new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }"); + }); + this.emitLine("},"); + }, true); + }); + } + + private emitDecoderTransformerCase( + tokenCases: string[], + variableName: string, + xfer: Transformer | undefined, + targetType: Type, + emitFinish: (value: Sourcelike) => void + ): void { + if (xfer === undefined) return; + + for (const tokenCase of tokenCases) { + this.emitTokenCase(tokenCase); + } + + this.indent(() => { + const allHandled = this.emitDecodeTransformer(xfer, targetType, emitFinish, variableName); + if (!allHandled) { + this.emitLine("break;"); + } + }); + } + + private emitConsume( + value: Sourcelike, + consumer: Transformer | undefined, + targetType: Type, + emitFinish: (variableName: Sourcelike) => void + ): boolean { + if (consumer === undefined) { + emitFinish(value); + return true; + } else { + return this.emitTransformer(value, consumer, targetType, emitFinish); + } + } + + private emitDecodeTransformer( + xfer: Transformer, + targetType: Type, + emitFinish: (value: Sourcelike) => void, + variableName = "value" + ): boolean { + if (xfer instanceof DecodingTransformer) { + const source = xfer.sourceType; + const converter = this.converterForType(targetType); + if (converter !== undefined) { + const typeSource = this.csType(targetType); + this.emitLine("var converter = ", this.converterObject(converter), ";"); + this.emitLine( + "var ", + variableName, + " = (", + typeSource, + ")converter.ReadJson(reader, typeof(", + typeSource, + "), null, serializer);" + ); + } else if (source.kind !== "null") { + let output = targetType.kind === "double" ? targetType : source; + this.emitLine("var ", variableName, " = ", this.deserializeTypeCode(this.csType(output)), ";"); + } + + return this.emitConsume(variableName, xfer.consumer, targetType, emitFinish); + } else if (xfer instanceof ArrayDecodingTransformer) { + // FIXME: Consume StartArray + if (!(targetType instanceof ArrayType)) { + return panic("Array decoding must produce an array type"); + } + + // FIXME: handle EOF + this.emitLine("reader.Read();"); + this.emitLine("var ", variableName, " = new List<", this.csType(targetType.items), ">();"); + this.emitLine("while (reader.TokenType != JsonToken.EndArray)"); + this.emitBlock(() => { + this.emitDecodeTransformer( + xfer.itemTransformer, + xfer.itemTargetType, + v => this.emitLine(variableName, ".Add(", v, ");"), + "arrayItem" + ); + // FIXME: handle EOF + this.emitLine("reader.Read();"); + }); + let result: Sourcelike = variableName; + if (!this._options.useList) { + result = [result, ".ToArray()"]; + } + + emitFinish(result); + return true; + } else if (xfer instanceof DecodingChoiceTransformer) { + this.emitDecoderSwitch(() => { + const nullTransformer = xfer.nullTransformer; + if (nullTransformer !== undefined) { + this.emitTokenCase("Null"); + this.indent(() => { + const allHandled = this.emitDecodeTransformer(nullTransformer, targetType, emitFinish, "null"); + if (!allHandled) { + this.emitLine("break"); + } + }); + } + + this.emitDecoderTransformerCase( + ["Integer"], + "integerValue", + xfer.integerTransformer, + targetType, + emitFinish + ); + this.emitDecoderTransformerCase( + xfer.integerTransformer === undefined ? ["Integer", "Float"] : ["Float"], + "doubleValue", + xfer.doubleTransformer, + targetType, + emitFinish + ); + this.emitDecoderTransformerCase(["Boolean"], "boolValue", xfer.boolTransformer, targetType, emitFinish); + this.emitDecoderTransformerCase( + ["String", "Date"], + "stringValue", + xfer.stringTransformer, + targetType, + emitFinish + ); + this.emitDecoderTransformerCase( + ["StartObject"], + "objectValue", + xfer.objectTransformer, + targetType, + emitFinish + ); + this.emitDecoderTransformerCase( + ["StartArray"], + "arrayValue", + xfer.arrayTransformer, + targetType, + emitFinish + ); + }); + return false; + } else { + return panic("Unknown transformer"); + } + } + + private stringCaseValue(t: Type, stringCase: string): Sourcelike { + if (t.kind === "string") { + return ['"', utf16StringEscape(stringCase), '"']; + } else if (t instanceof EnumType) { + return [this.nameForNamedType(t), ".", this.nameForEnumCase(t, stringCase)]; + } + + return panic(`Type ${t.kind} does not have string cases`); + } + + private emitTransformer( + variable: Sourcelike, + xfer: Transformer, + targetType: Type, + emitFinish: (value: Sourcelike) => void + ): boolean { + function directTargetType(continuation: Transformer | undefined): Type { + if (continuation === undefined) { + return targetType; + } + + return followTargetType(continuation.sourceType); + } + + if (xfer instanceof ChoiceTransformer) { + const caseXfers = xfer.transformers; + if (caseXfers.length > 1 && caseXfers.every(caseXfer => caseXfer instanceof StringMatchTransformer)) { + this.emitLine("switch (", variable, ")"); + this.emitBlock(() => { + for (const caseXfer of caseXfers) { + const matchXfer = caseXfer as StringMatchTransformer; + const value = this.stringCaseValue( + followTargetType(matchXfer.sourceType), + matchXfer.stringCase + ); + this.emitLine("case ", value, ":"); + this.indent(() => { + const allDone = this.emitTransformer( + variable, + matchXfer.transformer, + targetType, + emitFinish + ); + if (!allDone) { + this.emitLine("break;"); + } + }); + } + }); + // FIXME: Can we check for exhaustiveness? For enums it should be easy. + return false; + } else { + for (const caseXfer of caseXfers) { + this.emitTransformer(variable, caseXfer, targetType, emitFinish); + } + } + } else if (xfer instanceof UnionMemberMatchTransformer) { + const memberType = xfer.memberType; + const maybeNullable = nullableFromUnion(xfer.sourceType); + let test: Sourcelike; + let member: Sourcelike; + if (maybeNullable !== null) { + if (memberType.kind === "null") { + test = [variable, " == null"]; + member = "null"; + } else { + test = [variable, " != null"]; + member = variable; + } + } else if (memberType.kind === "null") { + test = [variable, ".IsNull"]; + member = "null"; + } else { + const memberName = this.nameForUnionMember(xfer.sourceType, memberType); + member = [variable, ".", memberName]; + test = [member, " != null"]; + } + + if (memberType.kind !== "null" && isValueType(memberType)) { + member = [member, ".Value"]; + } + + this.emitLine("if (", test, ")"); + this.emitBlock(() => this.emitTransformer(member, xfer.transformer, targetType, emitFinish)); + } else if (xfer instanceof StringMatchTransformer) { + const value = this.stringCaseValue(followTargetType(xfer.sourceType), xfer.stringCase); + this.emitLine("if (", variable, " == ", value, ")"); + this.emitBlock(() => this.emitTransformer(variable, xfer.transformer, targetType, emitFinish)); + } else if (xfer instanceof EncodingTransformer) { + const converter = this.converterForType(xfer.sourceType); + if (converter !== undefined) { + this.emitLine("var converter = ", this.converterObject(converter), ";"); + this.emitLine("converter.WriteJson(writer, ", variable, ", serializer);"); + } else { + this.emitLine(this.serializeValueCode(variable), ";"); + } + + emitFinish([]); + return true; + } else if (xfer instanceof ArrayEncodingTransformer) { + this.emitLine("writer.WriteStartArray();"); + const itemVariable = "arrayItem"; + this.emitLine("foreach (var ", itemVariable, " in ", variable, ")"); + this.emitBlock(() => { + this.emitTransformer(itemVariable, xfer.itemTransformer, xfer.itemTargetType, () => { + return; + }); + }); + this.emitLine("writer.WriteEndArray();"); + emitFinish([]); + return true; + } else if (xfer instanceof ParseStringTransformer) { + const immediateTargetType = xfer.consumer === undefined ? targetType : xfer.consumer.sourceType; + switch (immediateTargetType.kind) { + case "date-time": + this.emitLine("DateTimeOffset dt;"); + this.emitLine("if (DateTimeOffset.TryParse(", variable, ", out dt))"); + this.emitBlock(() => this.emitConsume("dt", xfer.consumer, targetType, emitFinish)); + break; + case "uuid": + this.emitLine("Guid guid;"); + this.emitLine("if (Guid.TryParse(", variable, ", out guid))"); + this.emitBlock(() => this.emitConsume("guid", xfer.consumer, targetType, emitFinish)); + break; + case "uri": + this.emitLine("try"); + this.emitBlock(() => { + this.emitLine("var uri = new Uri(", variable, ");"); + this.emitConsume("uri", xfer.consumer, targetType, emitFinish); + }); + this.emitLine("catch (UriFormatException) {}"); + break; + case "integer": + this.emitLine("long l;"); + this.emitLine("if (Int64.TryParse(", variable, ", out l))"); + this.emitBlock(() => this.emitConsume("l", xfer.consumer, targetType, emitFinish)); + break; + case "bool": + this.emitLine("bool b;"); + this.emitLine("if (Boolean.TryParse(", variable, ", out b))"); + this.emitBlock(() => this.emitConsume("b", xfer.consumer, targetType, emitFinish)); + break; + default: + return panic(`Parsing string to ${immediateTargetType.kind} not supported`); + } + } else if (xfer instanceof StringifyTransformer) { + switch (xfer.sourceType.kind) { + case "date-time": + return this.emitConsume( + [variable, '.ToString("o", System.Globalization.CultureInfo.InvariantCulture)'], + xfer.consumer, + targetType, + emitFinish + ); + case "uuid": + return this.emitConsume( + [variable, '.ToString("D", System.Globalization.CultureInfo.InvariantCulture)'], + xfer.consumer, + targetType, + emitFinish + ); + case "integer": + case "uri": + return this.emitConsume([variable, ".ToString()"], xfer.consumer, targetType, emitFinish); + case "bool": + this.emitLine("var boolString = ", variable, ' ? "true" : "false";'); + return this.emitConsume("boolString", xfer.consumer, targetType, emitFinish); + default: + return panic(`Stringifying ${xfer.sourceType.kind} not supported`); + } + } else if (xfer instanceof StringProducerTransformer) { + const value = this.stringCaseValue(directTargetType(xfer.consumer), xfer.result); + return this.emitConsume(value, xfer.consumer, targetType, emitFinish); + } else if (xfer instanceof MinMaxLengthCheckTransformer) { + const min = xfer.minLength; + const max = xfer.maxLength; + const conditions: Sourcelike[] = []; + + if (min !== undefined) { + conditions.push([variable, ".Length >= ", min.toString()]); + } + + if (max !== undefined) { + conditions.push([variable, ".Length <= ", max.toString()]); + } + + this.emitLine("if (", arrayIntercalate([" && "], conditions), ")"); + this.emitBlock(() => this.emitConsume(variable, xfer.consumer, targetType, emitFinish)); + return false; + } else if (xfer instanceof MinMaxValueTransformer) { + const min = xfer.minimum; + const max = xfer.maximum; + const conditions: Sourcelike[] = []; + + if (min !== undefined) { + conditions.push([variable, " >= ", min.toString()]); + } + + if (max !== undefined) { + conditions.push([variable, " <= ", max.toString()]); + } + + this.emitLine("if (", arrayIntercalate([" && "], conditions), ")"); + this.emitBlock(() => this.emitConsume(variable, xfer.consumer, targetType, emitFinish)); + return false; + } else if (xfer instanceof UnionInstantiationTransformer) { + if (!(targetType instanceof UnionType)) { + return panic("Union instantiation transformer must produce a union type"); + } + + const maybeNullable = nullableFromUnion(targetType); + if (maybeNullable !== null) { + emitFinish(variable); + } else { + const unionName = this.nameForNamedType(targetType); + let initializer: Sourcelike; + if (xfer.sourceType.kind === "null") { + initializer = " "; + } else { + const memberName = this.nameForUnionMember(targetType, xfer.sourceType); + initializer = [" ", memberName, " = ", variable, " "]; + } + + emitFinish(["new ", unionName, " {", initializer, "}"]); + } + + return true; + } else { + return panic("Unknown transformer"); + } + + return false; + } + + private emitTransformation(converterName: Name, t: Type): void { + const xf = defined(transformationForType(t)); + const reverse = xf.reverse; + const targetType = xf.targetType; + const xfer = xf.transformer; + this.emitType(undefined, AccessModifier.Internal, "class", converterName, "JsonConverter", () => { + const csType = this.csType(targetType); + let canConvertExpr: Sourcelike = ["t == typeof(", csType, ")"]; + const haveNullable = isValueType(targetType); + if (haveNullable) { + canConvertExpr = [canConvertExpr, " || t == typeof(", csType, "?)"]; + } + + this.emitCanConvert(canConvertExpr); + this.ensureBlankLine(); + this.emitReadJson(() => { + // FIXME: It's unsatisfying that we need this. The reason is that we not + // only match T, but also T?. If we didn't, then the T in T? would not be + // deserialized with our converter but with the default one. Can we check + // whether the type is a nullable? + // FIXME: This could duplicate one of the cases handled below in + // `emitDecodeTransformer`. + if (haveNullable && !(targetType instanceof UnionType)) { + this.emitLine("if (reader.TokenType == JsonToken.Null) return null;"); + } + + const allHandled = this.emitDecodeTransformer(xfer, targetType, v => this.emitLine("return ", v, ";")); + if (!allHandled) { + this.emitThrow(['"Cannot unmarshal type ', csType, '"']); + } + }); + this.ensureBlankLine(); + this.emitWriteJson("untypedValue", () => { + // FIXME: See above. + if (haveNullable && !(targetType instanceof UnionType)) { + this.emitLine("if (untypedValue == null)"); + this.emitBlock(() => { + this.emitLine("serializer.Serialize(writer, null);"); + this.emitLine("return;"); + }); + } + + this.emitLine("var value = (", csType, ")untypedValue;"); + const allHandled = this.emitTransformer("value", reverse.transformer, reverse.targetType, () => + this.emitLine("return;") + ); + if (!allHandled) { + this.emitThrow(['"Cannot marshal type ', csType, '"']); + } + }); + this.ensureBlankLine(); + this.emitLine("public static readonly ", converterName, " Singleton = new ", converterName, "();"); + }); + } + + protected emitRequiredHelpers(): void { + if (this._needHelpers) { + this.forEachTopLevel("leading-and-interposing", (t, n) => this.emitFromJsonForTopLevel(t, n)); + this.ensureBlankLine(); + this.emitSerializeClass(); + } + + if (this._needHelpers || (this._needAttributes && (this.haveNamedUnions || this.haveEnums))) { + this.ensureBlankLine(); + this.emitConverterClass(); + this.forEachTransformation("leading-and-interposing", (n, t) => this.emitTransformation(n, t)); + } + } + + protected needNamespace(): boolean { + return this._needNamespaces; + } +} diff --git a/packages/quicktype-core/src/language/CSharp/SystemTextJsonCSharpRenderer.ts b/packages/quicktype-core/src/language/CSharp/SystemTextJsonCSharpRenderer.ts new file mode 100644 index 000000000..77afed6da --- /dev/null +++ b/packages/quicktype-core/src/language/CSharp/SystemTextJsonCSharpRenderer.ts @@ -0,0 +1,982 @@ +import { arrayIntercalate } from "collection-utils"; + +import { type ForbiddenWordsInfo, inferredNameOrder } from "../../ConvenienceRenderer"; +import { DependencyName, type Name, SimpleName } from "../../Naming"; +import { type RenderContext } from "../../Renderer"; +import { type OptionValues } from "../../RendererOptions"; +import { type Sourcelike, modifySource } from "../../Source"; +import { camelCase, utf16StringEscape } from "../../support/Strings"; +import { defined, panic } from "../../support/Support"; +import { type TargetLanguage } from "../../TargetLanguage"; +import { + ArrayDecodingTransformer, + ArrayEncodingTransformer, + ChoiceTransformer, + DecodingChoiceTransformer, + DecodingTransformer, + EncodingTransformer, + MinMaxLengthCheckTransformer, + MinMaxValueTransformer, + ParseStringTransformer, + StringMatchTransformer, + StringProducerTransformer, + StringifyTransformer, + type Transformation, + type Transformer, + UnionInstantiationTransformer, + UnionMemberMatchTransformer, + followTargetType, + transformationForType +} from "../../Transformers"; +import { ArrayType, type ClassProperty, ClassType, EnumType, type Type, UnionType } from "../../Type"; +import { nullableFromUnion } from "../../TypeUtils"; + +import { CSharpRenderer } from "./CSharpRenderer"; +import { type systemTextJsonCSharpOptions } from "./language"; +import { + AccessModifier, + alwaysApplyTransformation, + denseJsonPropertyName, + denseNullValueHandlingEnumName, + isValueType, + namingFunction +} from "./utils"; + +export class SystemTextJsonCSharpRenderer extends CSharpRenderer { + private readonly _enumExtensionsNames = new Map(); + + private readonly _needHelpers: boolean; + + private readonly _needAttributes: boolean; + + private readonly _needNamespaces: boolean; + + public constructor( + targetLanguage: TargetLanguage, + renderContext: RenderContext, + private readonly _options: OptionValues + ) { + super(targetLanguage, renderContext, _options); + this._needHelpers = _options.features.helpers; + this._needAttributes = _options.features.attributes; + this._needNamespaces = _options.features.namespaces; + } + + protected forbiddenNamesForGlobalNamespace(): string[] { + const forbidden = [ + "Converter", + "JsonConverter", + "JsonSerializer", + "JsonWriter", + "JsonToken", + "Serialize", + "JsonSerializerOptions", + // "Newtonsoft", + // "MetadataPropertyHandling", + // "DateParseHandling", + "FromJson", + "Required" + ]; + if (this._options.dense) { + forbidden.push("J", "R", "N"); + } + + if (this._options.baseclass !== undefined) { + forbidden.push(this._options.baseclass); + } + + return super.forbiddenNamesForGlobalNamespace().concat(forbidden); + } + + protected forbiddenForObjectProperties(c: ClassType, className: Name): ForbiddenWordsInfo { + const result = super.forbiddenForObjectProperties(c, className); + result.names = result.names.concat(["ToJson", "FromJson", "Required"]); + return result; + } + + protected makeNameForTransformation(xf: Transformation, typeName: Name | undefined): Name { + if (typeName === undefined) { + let xfer = xf.transformer; + if (xfer instanceof DecodingTransformer && xfer.consumer !== undefined) { + xfer = xfer.consumer; + } + + return new SimpleName([`${xfer.kind}_converter`], namingFunction, inferredNameOrder + 30); + } + + return new DependencyName(namingFunction, typeName.order + 30, lookup => `${lookup(typeName)}_converter`); + } + + protected makeNamedTypeDependencyNames(t: Type, name: Name): DependencyName[] { + if (!(t instanceof EnumType)) return []; + + const extensionsName = new DependencyName( + namingFunction, + name.order + 30, + lookup => `${lookup(name)}_extensions` + ); + this._enumExtensionsNames.set(name, extensionsName); + return [extensionsName]; + } + + protected emitUsings(): void { + // FIXME: We need System.Collections.Generic whenever we have maps or use List. + if (!this._needAttributes && !this._needHelpers) return; + + super.emitUsings(); + this.ensureBlankLine(); + + for (const ns of ["System.Text.Json", "System.Text.Json.Serialization", "System.Globalization"]) { + this.emitUsing(ns); + } + + if (this._options.dense) { + this.emitUsing([denseJsonPropertyName, " = System.Text.Json.Serialization.JsonPropertyNameAttribute"]); + // this.emitUsing([denseRequiredEnumName, " = Newtonsoft.Json.Required"]); + this.emitUsing([denseNullValueHandlingEnumName, " = System.Text.Json.Serialization.JsonIgnoreCondition"]); + } + + if (this._options.baseclass === "EntityData") { + this.emitUsing("Microsoft.Azure.Mobile.Server"); + } + } + + protected baseclassForType(_t: Type): Sourcelike | undefined { + return this._options.baseclass; + } + + protected emitDefaultFollowingComments(): void { + if (!this._needHelpers) return; + + this.emitLine("#pragma warning restore CS8618"); + this.emitLine("#pragma warning restore CS8601"); + this.emitLine("#pragma warning restore CS8603"); + } + + protected emitDefaultLeadingComments(): void { + if (!this._needHelpers) return; + + this.emitLine("// "); + this.emitLine("//"); + this.emitLine( + "// To parse this JSON data, add NuGet 'System.Text.Json' then do", + this.topLevels.size === 1 ? "" : " one of these", + ":" + ); + this.emitLine("//"); + this.emitLine("// using ", this._options.namespace, ";"); + this.emitLine("//"); + this.forEachTopLevel("none", (t, topLevelName) => { + let rhs: Sourcelike; + if (t instanceof EnumType) { + rhs = ["JsonSerializer.Deserialize<", topLevelName, ">(jsonString)"]; + } else { + rhs = [topLevelName, ".FromJson(jsonString)"]; + } + + this.emitLine("// var ", modifySource(camelCase, topLevelName), " = ", rhs, ";"); + }); + + // fix: should this be an option? Or respond to an existing option? + this.emitLine("#nullable enable"); + this.emitLine("#pragma warning disable CS8618"); + this.emitLine("#pragma warning disable CS8601"); + this.emitLine("#pragma warning disable CS8603"); + } + + private converterForType(t: Type): Name | undefined { + let xf = transformationForType(t); + + if (xf === undefined && t instanceof UnionType) { + const maybeNullable = nullableFromUnion(t); + if (maybeNullable !== null) { + t = maybeNullable; + xf = transformationForType(t); + } + } + + if (xf === undefined) return undefined; + + if (alwaysApplyTransformation(xf)) return undefined; + + return defined(this.nameForTransformation(t)); + } + + protected attributesForProperty( + property: ClassProperty, + _name: Name, + _c: ClassType, + jsonName: string + ): Sourcelike[] | undefined { + if (!this._needAttributes) return undefined; + + const attributes: Sourcelike[] = []; + + const jsonPropertyName = this._options.dense ? denseJsonPropertyName : "JsonPropertyName"; + const escapedName = utf16StringEscape(jsonName); + const isNullable = followTargetType(property.type).isNullable; + const isOptional = property.isOptional; + + if (isOptional && !isNullable) { + attributes.push(["[", "JsonIgnore", "(Condition = JsonIgnoreCondition.WhenWritingNull)]"]); + } + + // const requiredClass = this._options.dense ? "R" : "Required"; + // const nullValueHandlingClass = this._options.dense ? "N" : "NullValueHandling"; + // const nullValueHandling = isOptional && !isNullable ? [", NullValueHandling = ", nullValueHandlingClass, ".Ignore"] : []; + // let required: Sourcelike; + // if (!this._options.checkRequired || (isOptional && isNullable)) { + // required = [nullValueHandling]; + // } else if (isOptional && !isNullable) { + // required = [", Required = ", requiredClass, ".DisallowNull", nullValueHandling]; + // } else if (!isOptional && isNullable) { + // required = [", Required = ", requiredClass, ".AllowNull"]; + // } else { + // required = [", Required = ", requiredClass, ".Always", nullValueHandling]; + // } + + attributes.push(["[", jsonPropertyName, '("', escapedName, '")]']); + + const converter = this.converterForType(property.type); + if (converter !== undefined) { + attributes.push(["[JsonConverter(typeof(", converter, "))]"]); + } + + return attributes; + } + + protected blankLinesBetweenAttributes(): boolean { + return this._needAttributes && !this._options.dense; + } + + // The "this" type can't be `dynamic`, so we have to force it to `object`. + private topLevelResultType(t: Type): Sourcelike { + return t.kind === "any" || t.kind === "none" ? "object" : this.csType(t); + } + + private emitFromJsonForTopLevel(t: Type, name: Name): void { + if (t instanceof EnumType) return; + + let partial: string; + let typeKind: string; + const definedType = this.namedTypeToNameForTopLevel(t); + if (definedType !== undefined) { + partial = "partial "; + typeKind = definedType instanceof ClassType ? "class" : "struct"; + } else { + partial = ""; + typeKind = "class"; + } + + const csType = this.topLevelResultType(t); + this.emitType(undefined, AccessModifier.Public, [partial, typeKind], name, this.baseclassForType(t), () => { + // FIXME: Make FromJson a Named + this.emitExpressionMember( + ["public static ", csType, " FromJson(string json)"], + ["JsonSerializer.Deserialize<", csType, ">(json, ", this._options.namespace, ".Converter.Settings)"] + ); + }); + } + + private emitDecoderSwitch(emitBody: () => void): void { + this.emitLine("switch (reader.TokenType)"); + this.emitBlock(emitBody); + } + + private emitTokenCase(tokenType: string): void { + this.emitLine("case JsonTokenType.", tokenType, ":"); + } + + private emitThrow(message: Sourcelike): void { + this.emitLine("throw new Exception(", message, ");"); + } + + private deserializeTypeCode(typeName: Sourcelike): Sourcelike { + switch (typeName) { + case "bool": + return ["reader.GetBoolean()"]; + case "long": + return ["reader.GetInt64()"]; + case "decimal": + return ["reader.GetDecimal()"]; + case "double": + return ["reader.GetDouble()"]; + case "string": + return ["reader.GetString()"]; + default: + return ["JsonSerializer.Deserialize<", typeName, ">(ref reader, options)"]; + } + } + + private serializeValueCode(value: Sourcelike): Sourcelike { + if (value !== "null") return ["JsonSerializer.Serialize(writer, ", value, ", options)"]; + else return ["writer.WriteNullValue()"]; + } + + private emitSerializeClass(): void { + // FIXME: Make Serialize a Named + this.emitType(undefined, AccessModifier.Public, "static class", "Serialize", undefined, () => { + // Sometimes multiple top-levels will resolve to the same type, so we have to take care + // not to emit more than one extension method for the same type. + const seenTypes = new Set(); + this.forEachTopLevel("none", t => { + // FIXME: Make ToJson a Named + if (!seenTypes.has(t)) { + seenTypes.add(t); + this.emitExpressionMember( + ["public static string ToJson(this ", this.topLevelResultType(t), " self)"], + ["JsonSerializer.Serialize(self, ", this._options.namespace, ".Converter.Settings)"] + ); + } + }); + }); + } + + private emitCanConvert(expr: Sourcelike): void { + this.emitExpressionMember("public override bool CanConvert(Type t)", expr); + } + + private emitReadJson(emitBody: () => void, csType: Sourcelike): void { + this.emitLine( + "public override ", + csType, + " Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)" + ); + this.emitBlock(emitBody); + } + + private emitWriteJson(variable: string, emitBody: () => void, csType: Sourcelike): void { + this.emitLine( + "public override void Write(Utf8JsonWriter writer, ", + csType, + " ", + variable, + ", JsonSerializerOptions options)" + ); + this.emitBlock(emitBody); + } + + private converterObject(converterName: Name): Sourcelike { + // FIXME: Get a singleton + return [converterName, ".Singleton"]; + } + + private emitConverterClass(): void { + // FIXME: Make Converter a Named + const converterName: Sourcelike = ["Converter"]; + this.emitType(undefined, AccessModifier.Internal, "static class", converterName, undefined, () => { + // Do not use .Web as defaults. That turns on caseInsensitive property names and will fail the keywords test. + this.emitLine( + "public static readonly JsonSerializerOptions Settings = new(JsonSerializerDefaults.General)" + ); + this.emitBlock(() => { + // this.emitLine("MetadataPropertyHandling = MetadataPropertyHandling.Ignore,"); + // this.emitLine("DateParseHandling = DateParseHandling.None,"); + this.emitLine("Converters ="); + this.emitLine("{"); + this.indent(() => { + for (const [t, converter] of this.typesWithNamedTransformations) { + if (alwaysApplyTransformation(defined(transformationForType(t)))) { + this.emitLine(this.converterObject(converter), ","); + } + } + + this.emitLine("new DateOnlyConverter(),"); + this.emitLine("new TimeOnlyConverter(),"); + this.emitLine("IsoDateTimeOffsetConverter.Singleton"); + // this.emitLine("new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }"); + }); + this.emitLine("},"); + }, true); + }); + } + + private emitDecoderTransformerCase( + tokenCases: string[], + variableName: string, + xfer: Transformer | undefined, + targetType: Type, + emitFinish: (value: Sourcelike) => void + ): void { + if (xfer === undefined) return; + + for (const tokenCase of tokenCases) { + this.emitTokenCase(tokenCase); + } + + this.indent(() => { + const allHandled = this.emitDecodeTransformer(xfer, targetType, emitFinish, variableName); + if (!allHandled) { + this.emitLine("break;"); + } + }); + } + + private emitConsume( + value: Sourcelike, + consumer: Transformer | undefined, + targetType: Type, + emitFinish: (variableName: Sourcelike) => void + ): boolean { + if (consumer === undefined) { + emitFinish(value); + return true; + } else { + return this.emitTransformer(value, consumer, targetType, emitFinish); + } + } + + private emitDecodeTransformer( + xfer: Transformer, + targetType: Type, + emitFinish: (value: Sourcelike) => void, + variableName = "value" + ): boolean { + if (xfer instanceof DecodingTransformer) { + const source = xfer.sourceType; + const converter = this.converterForType(targetType); + if (converter !== undefined) { + const typeSource = this.csType(targetType); + this.emitLine("var converter = ", this.converterObject(converter), ";"); + this.emitLine( + "var ", + variableName, + " = (", + typeSource, + ")converter.ReadJson(reader, typeof(", + typeSource, + "), null, serializer);" + ); + } else if (source.kind !== "null") { + let output = targetType.kind === "double" ? targetType : source; + this.emitLine("var ", variableName, " = ", this.deserializeTypeCode(this.csType(output)), ";"); + } + + return this.emitConsume(variableName, xfer.consumer, targetType, emitFinish); + } else if (xfer instanceof ArrayDecodingTransformer) { + // FIXME: Consume StartArray + if (!(targetType instanceof ArrayType)) { + return panic("Array decoding must produce an array type"); + } + + // FIXME: handle EOF + this.emitLine("reader.Read();"); + this.emitLine("var ", variableName, " = new List<", this.csType(targetType.items), ">();"); + this.emitLine("while (reader.TokenType != JsonToken.EndArray)"); + this.emitBlock(() => { + this.emitDecodeTransformer( + xfer.itemTransformer, + xfer.itemTargetType, + v => this.emitLine(variableName, ".Add(", v, ");"), + "arrayItem" + ); + // FIXME: handle EOF + this.emitLine("reader.Read();"); + }); + let result: Sourcelike = variableName; + if (!this._options.useList) { + result = [result, ".ToArray()"]; + } + + emitFinish(result); + return true; + } else if (xfer instanceof DecodingChoiceTransformer) { + this.emitDecoderSwitch(() => { + const nullTransformer = xfer.nullTransformer; + if (nullTransformer !== undefined) { + this.emitTokenCase("Null"); + this.indent(() => { + const allHandled = this.emitDecodeTransformer(nullTransformer, targetType, emitFinish, "null"); + if (!allHandled) { + this.emitLine("break"); + } + }); + } + + this.emitDecoderTransformerCase( + ["Number"], + "integerValue", + xfer.integerTransformer, + targetType, + emitFinish + ); + this.emitDecoderTransformerCase( + ["Number"], + // xfer.integerTransformer === undefined ? ["Integer", "Float"] : ["Float"], + "doubleValue", + xfer.doubleTransformer, + targetType, + emitFinish + ); + this.emitDecoderTransformerCase( + ["True", "False"], + "boolValue", + xfer.boolTransformer, + targetType, + emitFinish + ); + this.emitDecoderTransformerCase( + // ["String", "Date"], + ["String"], + "stringValue", + xfer.stringTransformer, + targetType, + emitFinish + ); + this.emitDecoderTransformerCase( + ["StartObject"], + "objectValue", + xfer.objectTransformer, + targetType, + emitFinish + ); + this.emitDecoderTransformerCase( + ["StartArray"], + "arrayValue", + xfer.arrayTransformer, + targetType, + emitFinish + ); + }); + return false; + } else { + return panic("Unknown transformer"); + } + } + + private stringCaseValue(t: Type, stringCase: string): Sourcelike { + if (t.kind === "string") { + return ['"', utf16StringEscape(stringCase), '"']; + } else if (t instanceof EnumType) { + return [this.nameForNamedType(t), ".", this.nameForEnumCase(t, stringCase)]; + } + + return panic(`Type ${t.kind} does not have string cases`); + } + + private emitTransformer( + variable: Sourcelike, + xfer: Transformer, + targetType: Type, + emitFinish: (value: Sourcelike) => void + ): boolean { + function directTargetType(continuation: Transformer | undefined): Type { + if (continuation === undefined) { + return targetType; + } + + return followTargetType(continuation.sourceType); + } + + if (xfer instanceof ChoiceTransformer) { + const caseXfers = xfer.transformers; + if (caseXfers.length > 1 && caseXfers.every(caseXfer => caseXfer instanceof StringMatchTransformer)) { + this.emitLine("switch (", variable, ")"); + this.emitBlock(() => { + for (const caseXfer of caseXfers) { + const matchXfer = caseXfer as StringMatchTransformer; + const value = this.stringCaseValue( + followTargetType(matchXfer.sourceType), + matchXfer.stringCase + ); + this.emitLine("case ", value, ":"); + this.indent(() => { + const allDone = this.emitTransformer( + variable, + matchXfer.transformer, + targetType, + emitFinish + ); + if (!allDone) { + this.emitLine("break;"); + } + }); + } + }); + // FIXME: Can we check for exhaustiveness? For enums it should be easy. + return false; + } else { + for (const caseXfer of caseXfers) { + this.emitTransformer(variable, caseXfer, targetType, emitFinish); + } + } + } else if (xfer instanceof UnionMemberMatchTransformer) { + const memberType = xfer.memberType; + const maybeNullable = nullableFromUnion(xfer.sourceType); + let test: Sourcelike; + let member: Sourcelike; + if (maybeNullable !== null) { + if (memberType.kind === "null") { + test = [variable, " == null"]; + member = "null"; + } else { + test = [variable, " != null"]; + member = variable; + } + } else if (memberType.kind === "null") { + test = [variable, ".IsNull"]; + member = "null"; + } else { + const memberName = this.nameForUnionMember(xfer.sourceType, memberType); + member = [variable, ".", memberName]; + test = [member, " != null"]; + } + + if (memberType.kind !== "null" && isValueType(memberType)) { + member = [member, ".Value"]; + } + + this.emitLine("if (", test, ")"); + this.emitBlock(() => this.emitTransformer(member, xfer.transformer, targetType, emitFinish)); + } else if (xfer instanceof StringMatchTransformer) { + const value = this.stringCaseValue(followTargetType(xfer.sourceType), xfer.stringCase); + this.emitLine("if (", variable, " == ", value, ")"); + this.emitBlock(() => this.emitTransformer(variable, xfer.transformer, targetType, emitFinish)); + } else if (xfer instanceof EncodingTransformer) { + const converter = this.converterForType(xfer.sourceType); + if (converter !== undefined) { + this.emitLine("var converter = ", this.converterObject(converter), ";"); + this.emitLine("converter.WriteJson(writer, ", variable, ", serializer);"); + } else { + this.emitLine(this.serializeValueCode(variable), ";"); + } + + emitFinish([]); + return true; + } else if (xfer instanceof ArrayEncodingTransformer) { + this.emitLine("writer.WriteStartArray();"); + const itemVariable = "arrayItem"; + this.emitLine("foreach (var ", itemVariable, " in ", variable, ")"); + this.emitBlock(() => { + this.emitTransformer(itemVariable, xfer.itemTransformer, xfer.itemTargetType, () => { + return; + }); + }); + this.emitLine("writer.WriteEndArray();"); + emitFinish([]); + return true; + } else if (xfer instanceof ParseStringTransformer) { + const immediateTargetType = xfer.consumer === undefined ? targetType : xfer.consumer.sourceType; + switch (immediateTargetType.kind) { + case "date-time": + this.emitLine("DateTimeOffset dt;"); + this.emitLine("if (DateTimeOffset.TryParse(", variable, ", out dt))"); + this.emitBlock(() => this.emitConsume("dt", xfer.consumer, targetType, emitFinish)); + break; + case "uuid": + this.emitLine("Guid guid;"); + this.emitLine("if (Guid.TryParse(", variable, ", out guid))"); + this.emitBlock(() => this.emitConsume("guid", xfer.consumer, targetType, emitFinish)); + break; + case "uri": + this.emitLine("try"); + this.emitBlock(() => { + // this.emitLine("var uri = new Uri(", variable, ");"); + // The default value about:blank should never happen, but this way we avoid a null reference warning. + this.emitLine('var uri = new Uri("about:blank");'); + this.emitLine("if (!string.IsNullOrEmpty(stringValue))"); + this.emitBlock(() => { + this.emitLine("uri = new Uri(", variable, ");"); + }); + this.emitConsume("uri", xfer.consumer, targetType, emitFinish); + }); + this.emitLine("catch (UriFormatException) {}"); + break; + case "integer": + this.emitLine("long l;"); + this.emitLine("if (Int64.TryParse(", variable, ", out l))"); + this.emitBlock(() => this.emitConsume("l", xfer.consumer, targetType, emitFinish)); + break; + case "bool": + this.emitLine("bool b;"); + this.emitLine("if (Boolean.TryParse(", variable, ", out b))"); + this.emitBlock(() => this.emitConsume("b", xfer.consumer, targetType, emitFinish)); + break; + default: + return panic(`Parsing string to ${immediateTargetType.kind} not supported`); + } + } else if (xfer instanceof StringifyTransformer) { + switch (xfer.sourceType.kind) { + case "date-time": + return this.emitConsume( + [variable, '.ToString("o", System.Globalization.CultureInfo.InvariantCulture)'], + xfer.consumer, + targetType, + emitFinish + ); + case "uuid": + return this.emitConsume( + [variable, '.ToString("D", System.Globalization.CultureInfo.InvariantCulture)'], + xfer.consumer, + targetType, + emitFinish + ); + case "integer": + case "uri": + return this.emitConsume([variable, ".ToString()"], xfer.consumer, targetType, emitFinish); + case "bool": + this.emitLine("var boolString = ", variable, ' ? "true" : "false";'); + return this.emitConsume("boolString", xfer.consumer, targetType, emitFinish); + default: + return panic(`Stringifying ${xfer.sourceType.kind} not supported`); + } + } else if (xfer instanceof StringProducerTransformer) { + const value = this.stringCaseValue(directTargetType(xfer.consumer), xfer.result); + return this.emitConsume(value, xfer.consumer, targetType, emitFinish); + } else if (xfer instanceof MinMaxLengthCheckTransformer) { + const min = xfer.minLength; + const max = xfer.maxLength; + const conditions: Sourcelike[] = []; + + if (min !== undefined) { + conditions.push([variable, ".Length >= ", min.toString()]); + } + + if (max !== undefined) { + conditions.push([variable, ".Length <= ", max.toString()]); + } + + this.emitLine("if (", arrayIntercalate([" && "], conditions), ")"); + this.emitBlock(() => this.emitConsume(variable, xfer.consumer, targetType, emitFinish)); + return false; + } else if (xfer instanceof MinMaxValueTransformer) { + const min = xfer.minimum; + const max = xfer.maximum; + const conditions: Sourcelike[] = []; + + if (min !== undefined) { + conditions.push([variable, " >= ", min.toString()]); + } + + if (max !== undefined) { + conditions.push([variable, " <= ", max.toString()]); + } + + this.emitLine("if (", arrayIntercalate([" && "], conditions), ")"); + this.emitBlock(() => this.emitConsume(variable, xfer.consumer, targetType, emitFinish)); + return false; + } else if (xfer instanceof UnionInstantiationTransformer) { + if (!(targetType instanceof UnionType)) { + return panic("Union instantiation transformer must produce a union type"); + } + + const maybeNullable = nullableFromUnion(targetType); + if (maybeNullable !== null) { + emitFinish(variable); + } else { + const unionName = this.nameForNamedType(targetType); + let initializer: Sourcelike; + if (xfer.sourceType.kind === "null") { + initializer = " "; + } else { + const memberName = this.nameForUnionMember(targetType, xfer.sourceType); + initializer = [" ", memberName, " = ", variable, " "]; + } + + emitFinish(["new ", unionName, " {", initializer, "}"]); + } + + return true; + } else { + return panic("Unknown transformer"); + } + + return false; + } + + private emitTransformation(converterName: Name, t: Type): void { + const xf = defined(transformationForType(t)); + const reverse = xf.reverse; + const targetType = xf.targetType; + const xfer = xf.transformer; + const csType = this.csType(targetType); + // const haveNullable = isValueType(targetType); + + // if (haveNullable) { + // converterName = ['Nullable', converterName]; + // csType = [csType, "?"]; + // } + this.emitType( + undefined, + AccessModifier.Internal, + "class", + converterName, + ["JsonConverter<", csType, ">"], + () => { + let canConvertExpr: Sourcelike = ["t == typeof(", csType, ")"]; + this.emitCanConvert(canConvertExpr); + this.ensureBlankLine(); + this.emitReadJson(() => { + // FIXME: It's unsatisfying that we need this. The reason is that we not + // only match T, but also T?. If we didn't, then the T in T? would not be + // deserialized with our converter but with the default one. Can we check + // whether the type is a nullable? + // FIXME: This could duplicate one of the cases handled below in + // `emitDecodeTransformer`. + // if (haveNullable && !(targetType instanceof UnionType)) { + // this.emitLine("if (reader.TokenType == JsonTokenType.Null) return null;"); + // } + + const allHandled = this.emitDecodeTransformer(xfer, targetType, v => + this.emitLine("return ", v, ";") + ); + if (!allHandled) { + this.emitThrow(['"Cannot unmarshal type ', csType, '"']); + } + }, csType); + this.ensureBlankLine(); + this.emitWriteJson( + "value", + () => { + // FIXME: See above. + // if (haveNullable && !(targetType instanceof UnionType)) { + // this.emitLine("if (value == null)"); + // this.emitBlock(() => { + // this.emitLine("writer.WriteNullValue();"); + // this.emitLine("return;"); + // }); + // } + + const allHandled = this.emitTransformer("value", reverse.transformer, reverse.targetType, () => + this.emitLine("return;") + ); + if (!allHandled) { + this.emitThrow(['"Cannot marshal type ', csType, '"']); + } + }, + csType + ); + this.ensureBlankLine(); + this.emitLine("public static readonly ", converterName, " Singleton = new ", converterName, "();"); + } + ); + } + + protected emitRequiredHelpers(): void { + if (this._needHelpers) { + this.forEachTopLevel("leading-and-interposing", (t, n) => this.emitFromJsonForTopLevel(t, n)); + this.ensureBlankLine(); + this.emitSerializeClass(); + } + + if (this._needHelpers || (this._needAttributes && (this.haveNamedUnions || this.haveEnums))) { + this.ensureBlankLine(); + this.emitConverterClass(); + this.forEachTransformation("leading-and-interposing", (n, t) => this.emitTransformation(n, t)); + this.emitMultiline(` +public class DateOnlyConverter : JsonConverter +{ + private readonly string serializationFormat; + public DateOnlyConverter() : this(null) { } + + public DateOnlyConverter(string? serializationFormat) + { + this.serializationFormat = serializationFormat ?? "yyyy-MM-dd"; + } + + public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + return DateOnly.Parse(value!); + } + + public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString(serializationFormat)); +} + +public class TimeOnlyConverter : JsonConverter +{ + private readonly string serializationFormat; + + public TimeOnlyConverter() : this(null) { } + + public TimeOnlyConverter(string? serializationFormat) + { + this.serializationFormat = serializationFormat ?? "HH:mm:ss.fff"; + } + + public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + return TimeOnly.Parse(value!); + } + + public override void Write(Utf8JsonWriter writer, TimeOnly value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString(serializationFormat)); +} + +internal class IsoDateTimeOffsetConverter : JsonConverter +{ + public override bool CanConvert(Type t) => t == typeof(DateTimeOffset); + + private const string DefaultDateTimeFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFFK"; + + private DateTimeStyles _dateTimeStyles = DateTimeStyles.RoundtripKind; + private string? _dateTimeFormat; + private CultureInfo? _culture; + + public DateTimeStyles DateTimeStyles + { + get => _dateTimeStyles; + set => _dateTimeStyles = value; + } + + public string? DateTimeFormat + { + get => _dateTimeFormat ?? string.Empty; + set => _dateTimeFormat = (string.IsNullOrEmpty(value)) ? null : value; + } + + public CultureInfo Culture + { + get => _culture ?? CultureInfo.CurrentCulture; + set => _culture = value; + } + + public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) + { + string text; + + + if ((_dateTimeStyles & DateTimeStyles.AdjustToUniversal) == DateTimeStyles.AdjustToUniversal + || (_dateTimeStyles & DateTimeStyles.AssumeUniversal) == DateTimeStyles.AssumeUniversal) + { + value = value.ToUniversalTime(); + } + + text = value.ToString(_dateTimeFormat ?? DefaultDateTimeFormat, Culture); + + writer.WriteStringValue(text); + } + + public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + string? dateText = reader.GetString(); + + if (string.IsNullOrEmpty(dateText) == false) + { + if (!string.IsNullOrEmpty(_dateTimeFormat)) + { + return DateTimeOffset.ParseExact(dateText, _dateTimeFormat, Culture, _dateTimeStyles); + } + else + { + return DateTimeOffset.Parse(dateText, Culture, _dateTimeStyles); + } + } + else + { + return default(DateTimeOffset); + } + } + + + public static readonly IsoDateTimeOffsetConverter Singleton = new IsoDateTimeOffsetConverter(); +}`); + } + } + + protected needNamespace(): boolean { + return this._needNamespaces; + } +} diff --git a/packages/quicktype-core/src/language/CSharp/constants.ts b/packages/quicktype-core/src/language/CSharp/constants.ts new file mode 100644 index 000000000..bbe7f3dfb --- /dev/null +++ b/packages/quicktype-core/src/language/CSharp/constants.ts @@ -0,0 +1,79 @@ +export const keywords = [ + "abstract", + "as", + "base", + "bool", + "break", + "byte", + "case", + "catch", + "char", + "checked", + "class", + "const", + "continue", + "decimal", + "default", + "delegate", + "do", + "double", + "else", + "enum", + "event", + "explicit", + "extern", + "false", + "finally", + "fixed", + "float", + "for", + "foreach", + "goto", + "if", + "implicit", + "in", + "int", + "interface", + "internal", + "is", + "lock", + "long", + "namespace", + "new", + "null", + "object", + "operator", + "out", + "override", + "params", + "private", + "protected", + "public", + "readonly", + "ref", + "return", + "sbyte", + "sealed", + "short", + "sizeof", + "stackalloc", + "static", + "string", + "struct", + "switch", + "this", + "throw", + "true", + "try", + "typeof", + "uint", + "ulong", + "unchecked", + "unsafe", + "ushort", + "using", + "virtual", + "void", + "volatile", + "while" +] as const; diff --git a/packages/quicktype-core/src/language/CSharp/index.ts b/packages/quicktype-core/src/language/CSharp/index.ts new file mode 100644 index 000000000..c9f7c6d8c --- /dev/null +++ b/packages/quicktype-core/src/language/CSharp/index.ts @@ -0,0 +1,4 @@ +export { CSharpTargetLanguage, cSharpOptions, newtonsoftCSharpOptions, systemTextJsonCSharpOptions } from "./language"; +export { CSharpRenderer } from "./CSharpRenderer"; +export { NewtonsoftCSharpRenderer } from "./NewtonSoftCSharpRenderer"; +export { SystemTextJsonCSharpRenderer } from "./SystemTextJsonCSharpRenderer"; diff --git a/packages/quicktype-core/src/language/CSharp/language.ts b/packages/quicktype-core/src/language/CSharp/language.ts new file mode 100644 index 000000000..e2b460582 --- /dev/null +++ b/packages/quicktype-core/src/language/CSharp/language.ts @@ -0,0 +1,175 @@ +import { type ConvenienceRenderer } from "../../ConvenienceRenderer"; +import { type RenderContext } from "../../Renderer"; +import { BooleanOption, EnumOption, type Option, StringOption, getOptionValues } from "../../RendererOptions"; +import { assertNever } from "../../support/Support"; +import { TargetLanguage } from "../../TargetLanguage"; +import { type PrimitiveStringTypeKind, type TransformedStringTypeKind, type Type } from "../../Type"; +import { type StringTypeMapping } from "../../TypeBuilder"; +import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../../types"; + +import { NewtonsoftCSharpRenderer } from "./NewtonSoftCSharpRenderer"; +import { SystemTextJsonCSharpRenderer } from "./SystemTextJsonCSharpRenderer"; +import { needTransformerForType } from "./utils"; + +export enum Framework { + Newtonsoft = "Newtonsoft", + SystemTextJson = "SystemTextJson" +} + +export type Version = 5 | 6; +export interface OutputFeatures { + attributes: boolean; + helpers: boolean; +} + +export type CSharpTypeForAny = "object" | "dynamic"; + +export const cSharpOptions = { + framework: new EnumOption( + "framework", + "Serialization framework", + [ + ["NewtonSoft", Framework.Newtonsoft], + ["SystemTextJson", Framework.SystemTextJson] + ], + "NewtonSoft" + ), + useList: new EnumOption("array-type", "Use T[] or List", [ + ["array", false], + ["list", true] + ]), + dense: new EnumOption( + "density", + "Property density", + [ + ["normal", false], + ["dense", true] + ], + "normal", + "secondary" + ), + // FIXME: Do this via a configurable named eventually. + namespace: new StringOption("namespace", "Generated namespace", "NAME", "QuickType"), + version: new EnumOption( + "csharp-version", + "C# version", + [ + ["5", 5], + ["6", 6] + ], + "6", + "secondary" + ), + virtual: new BooleanOption("virtual", "Generate virtual properties", false), + typeForAny: new EnumOption( + "any-type", + 'Type to use for "any"', + [ + ["object", "object"], + ["dynamic", "dynamic"] + ], + "object", + "secondary" + ), + useDecimal: new EnumOption( + "number-type", + "Type to use for numbers", + [ + ["double", false], + ["decimal", true] + ], + "double", + "secondary" + ), + features: new EnumOption("features", "Output features", [ + ["complete", { namespaces: true, helpers: true, attributes: true }], + ["attributes-only", { namespaces: true, helpers: false, attributes: true }], + ["just-types-and-namespace", { namespaces: true, helpers: false, attributes: false }], + ["just-types", { namespaces: true, helpers: false, attributes: false }] + ]), + baseclass: new EnumOption( + "base-class", + "Base class", + [ + ["EntityData", "EntityData"], + ["Object", undefined] + ], + "Object", + "secondary" + ), + checkRequired: new BooleanOption("check-required", "Fail if required properties are missing", false), + keepPropertyName: new BooleanOption("keep-property-name", "Keep original field name generate", false) +}; + +export const newtonsoftCSharpOptions = Object.assign({}, cSharpOptions, {}); + +export const systemTextJsonCSharpOptions = Object.assign({}, cSharpOptions, {}); + +export class CSharpTargetLanguage extends TargetLanguage { + public constructor() { + super("C#", ["cs", "csharp"], "cs"); + } + + protected getOptions(): Array> { + return [ + cSharpOptions.framework, + cSharpOptions.namespace, + cSharpOptions.version, + cSharpOptions.dense, + cSharpOptions.useList, + cSharpOptions.useDecimal, + cSharpOptions.typeForAny, + cSharpOptions.virtual, + cSharpOptions.features, + cSharpOptions.baseclass, + cSharpOptions.checkRequired, + cSharpOptions.keepPropertyName + ]; + } + + public get stringTypeMapping(): StringTypeMapping { + const mapping: Map = new Map(); + mapping.set("date", "date-time"); + mapping.set("time", "date-time"); + mapping.set("date-time", "date-time"); + mapping.set("uuid", "uuid"); + mapping.set("uri", "uri"); + mapping.set("integer-string", "integer-string"); + mapping.set("bool-string", "bool-string"); + return mapping; + } + + public get supportsUnionsWithBothNumberTypes(): boolean { + return true; + } + + public get supportsOptionalClassProperties(): boolean { + return true; + } + + public needsTransformerForType(t: Type): boolean { + const need = needTransformerForType(t); + return need !== "none" && need !== "nullable"; + } + + protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): ConvenienceRenderer { + const options = getOptionValues(cSharpOptions, untypedOptionValues); + + switch (options.framework) { + case Framework.Newtonsoft: + return new NewtonsoftCSharpRenderer( + this, + renderContext, + getOptionValues(newtonsoftCSharpOptions, untypedOptionValues) + ); + case Framework.SystemTextJson: + return new SystemTextJsonCSharpRenderer( + this, + renderContext, + getOptionValues(systemTextJsonCSharpOptions, untypedOptionValues) + ); + default: + return assertNever(options.framework); + } + } +} diff --git a/packages/quicktype-core/src/language/CSharp/utils.ts b/packages/quicktype-core/src/language/CSharp/utils.ts new file mode 100644 index 000000000..856de95ca --- /dev/null +++ b/packages/quicktype-core/src/language/CSharp/utils.ts @@ -0,0 +1,147 @@ +import unicode from "unicode-properties"; + +import { minMaxLengthForType, minMaxValueForType } from "../../attributes/Constraints"; +import { funPrefixNamer } from "../../Naming"; +import { type Sourcelike } from "../../Source"; +import { + type WordInName, + combineWords, + firstUpperWordStyle, + splitIntoWords, + utf16LegalizeCharacters +} from "../../support/Strings"; +import { panic } from "../../support/Support"; +import { type Transformation } from "../../Transformers"; +import { ArrayType, EnumType, type PrimitiveType, type Type, UnionType } from "../../Type"; +import { nullableFromUnion } from "../../TypeUtils"; + +import { keywords } from "./constants"; + +export enum AccessModifier { + None = "None", + Public = "Public", + Internal = "Internal" +} + +export function noFollow(t: Type): Type { + return t; +} + +export function needTransformerForType(t: Type): "automatic" | "manual" | "nullable" | "none" { + if (t instanceof UnionType) { + const maybeNullable = nullableFromUnion(t); + if (maybeNullable === null) return "automatic"; + if (needTransformerForType(maybeNullable) === "manual") return "nullable"; + return "none"; + } + + if (t instanceof ArrayType) { + const itemsNeed = needTransformerForType(t.items); + if (itemsNeed === "manual" || itemsNeed === "nullable") return "automatic"; + return "none"; + } + + if (t instanceof EnumType) return "automatic"; + if (t.kind === "double") return minMaxValueForType(t) !== undefined ? "manual" : "none"; + if (t.kind === "integer-string" || t.kind === "bool-string") return "manual"; + if (t.kind === "string") { + return minMaxLengthForType(t) !== undefined ? "manual" : "none"; + } + + return "none"; +} + +export function alwaysApplyTransformation(xf: Transformation): boolean { + const t = xf.targetType; + if (t instanceof EnumType) return true; + if (t instanceof UnionType) return nullableFromUnion(t) === null; + return false; +} + +/** + * The C# type for a given transformed string type. + */ +export function csTypeForTransformedStringType(t: PrimitiveType): Sourcelike { + switch (t.kind) { + case "date-time": + return "DateTimeOffset"; + case "uuid": + return "Guid"; + case "uri": + return "Uri"; + default: + return panic(`Transformed string type ${t.kind} not supported`); + } +} + +export const namingFunction = funPrefixNamer("namer", csNameStyle); +export const namingFunctionKeep = funPrefixNamer("namerKeep", csNameStyleKeep); + +// FIXME: Make a Named? +export const denseJsonPropertyName = "J"; +export const denseRequiredEnumName = "R"; +export const denseNullValueHandlingEnumName = "N"; + +export function isStartCharacter(utf16Unit: number): boolean { + if (unicode.isAlphabetic(utf16Unit)) { + return true; + } + + return utf16Unit === 0x5f; // underscore +} + +function isPartCharacter(utf16Unit: number): boolean { + const category: string = unicode.getCategory(utf16Unit); + if (["Nd", "Pc", "Mn", "Mc"].includes(category)) { + return true; + } + + return isStartCharacter(utf16Unit); +} + +const legalizeName = utf16LegalizeCharacters(isPartCharacter); + +export function csNameStyle(original: string): string { + const words = splitIntoWords(original); + return combineWords( + words, + legalizeName, + firstUpperWordStyle, + firstUpperWordStyle, + firstUpperWordStyle, + firstUpperWordStyle, + "", + isStartCharacter + ); +} + +function csNameStyleKeep(original: string): string { + const words: WordInName[] = [ + { + word: original, + isAcronym: false + } + ]; + + const result = combineWords( + words, + legalizeName, + x => x, + x => x, + x => x, + x => x, + "", + isStartCharacter + ); + + // @ts-expect-error needs strong type + return keywords.includes(result) ? "@" + result : result; +} + +export function isValueType(t: Type): boolean { + if (t instanceof UnionType) { + return nullableFromUnion(t) === null; + } + + return ["integer", "double", "bool", "enum", "date-time", "uuid"].includes(t.kind); +} diff --git a/packages/quicktype-core/src/language/Crystal.ts b/packages/quicktype-core/src/language/Crystal/CrystalRenderer.ts similarity index 56% rename from packages/quicktype-core/src/language/Crystal.ts rename to packages/quicktype-core/src/language/Crystal/CrystalRenderer.ts index 9816191bc..e699b9b9b 100644 --- a/packages/quicktype-core/src/language/Crystal.ts +++ b/packages/quicktype-core/src/language/Crystal/CrystalRenderer.ts @@ -1,234 +1,14 @@ -import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../Annotation"; -import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../ConvenienceRenderer"; -import { type Name, type Namer, funPrefixNamer } from "../Naming"; -import { type RenderContext } from "../Renderer"; -import { type Option } from "../RendererOptions"; -import { type Sourcelike, maybeAnnotated } from "../Source"; -import { - allLowerWordStyle, - combineWords, - escapeNonPrintableMapper, - firstUpperWordStyle, - intToHex, - isAscii, - isLetterOrUnderscore, - isLetterOrUnderscoreOrDigit, - isPrintable, - legalizeCharacters, - splitIntoWords, - utf32ConcatMap -} from "../support/Strings"; -import { TargetLanguage } from "../TargetLanguage"; -import { type ClassType, type EnumType, type Type, type UnionType } from "../Type"; -import { type FixMeOptionsAnyType } from "../types"; -import { matchType, nullableFromUnion, removeNullFromUnion } from "../TypeUtils"; - -export class CrystalTargetLanguage extends TargetLanguage { - protected makeRenderer(renderContext: RenderContext): CrystalRenderer { - return new CrystalRenderer(this, renderContext); - } - - public constructor() { - super("Crystal", ["crystal", "cr", "crystallang"], "cr"); - } - - protected get defaultIndentation(): string { - return " "; - } - - protected getOptions(): Array> { - return []; - } -} - -const keywords = [ - "Any", - "Array", - "Atomic", - "Bool", - "Channel", - "Char", - "Class", - "Enum", - "Enumerable", - "Event", - "Extern", - "Exception", - "File", - "Float", - "Float32", - "Float64", - "GC", - "GZip", - "Hash", - "HTML", - "HTTP", - "Int", - "Int128", - "Int16", - "Int32", - "Int64", - "Int8", - "Iterable", - "Link", - "Logger", - "Math", - "Mutex", - "Nil", - "Number", - "JSON", - "IO", - "Object", - "Pointer", - "Proc", - "Process", - "Range", - "Random", - "Regex", - "Reference", - "Set", - "Signal", - "Slice", - "Spec", - "StaticArray", - "String", - "Struct", - "Symbol", - "System", - "TCPServer", - "TCPSocket", - "Socket", - "Tempfile", - "Termios", - "Time", - "Tuple", - "ThreadLocal", - "UDPSocket", - "UInt128", - "UInt16", - "UInt32", - "UInt64", - "UInt8", - "Union", - "UNIXServer", - "UNIXSocket", - "UUID", - "URI", - "VaList", - "Value", - "Void", - "WeakRef", - "XML", - "YAML", - "Zip", - "Zlib", - "abstract", - "alias", - "as", - "as?", - "asm", - "begin", - "break", - "case", - "class", - "def", - "do", - "else", - "elsif", - "end", - "ensure", - "enum", - "extend", - "false", - "for", - "fun", - "if", - "in", - "include", - "instance_sizeof", - "is_a?", - "lib", - "macro", - "module", - "next", - "nil", - "nil?", - "of", - "out", - "pointerof", - "private", - "protected", - "require", - "rescue", - "return", - "select", - "self", - "sizeof", - "struct", - "super", - "then", - "true", - "type", - "typeof", - "uninitialized", - "union", - "unless", - "until", - "when", - "while", - "with", - "yield" -]; - -function isAsciiLetterOrUnderscoreOrDigit(codePoint: number): boolean { - if (!isAscii(codePoint)) { - return false; - } - - return isLetterOrUnderscoreOrDigit(codePoint); -} - -function isAsciiLetterOrUnderscore(codePoint: number): boolean { - if (!isAscii(codePoint)) { - return false; - } - - return isLetterOrUnderscore(codePoint); -} - -const legalizeName = legalizeCharacters(isAsciiLetterOrUnderscoreOrDigit); - -function crystalStyle(original: string, isSnakeCase: boolean): string { - const words = splitIntoWords(original); - - const wordStyle = isSnakeCase ? allLowerWordStyle : firstUpperWordStyle; - - const combined = combineWords( - words, - legalizeName, - wordStyle, - wordStyle, - wordStyle, - wordStyle, - isSnakeCase ? "_" : "", - isAsciiLetterOrUnderscore - ); - - return combined === "_" ? "_underscore" : combined; -} - -const snakeNamingFunction = funPrefixNamer("default", (original: string) => crystalStyle(original, true)); -const camelNamingFunction = funPrefixNamer("camel", (original: string) => crystalStyle(original, false)); - -function standardUnicodeCrystalEscape(codePoint: number): string { - if (codePoint <= 0xffff) { - return "\\u{" + intToHex(codePoint, 4) + "}"; - } else { - return "\\u{" + intToHex(codePoint, 6) + "}"; - } -} - -const crystalStringEscape = utf32ConcatMap(escapeNonPrintableMapper(isPrintable, standardUnicodeCrystalEscape)); +import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../../Annotation"; +import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../../ConvenienceRenderer"; +import { type Name, type Namer } from "../../Naming"; +import { type RenderContext } from "../../Renderer"; +import { type Sourcelike, maybeAnnotated } from "../../Source"; +import { type TargetLanguage } from "../../TargetLanguage"; +import { type ClassType, type EnumType, type Type, type UnionType } from "../../Type"; +import { matchType, nullableFromUnion, removeNullFromUnion } from "../../TypeUtils"; + +import { keywords } from "./constants"; +import { camelNamingFunction, crystalStringEscape, snakeNamingFunction } from "./utils"; export class CrystalRenderer extends ConvenienceRenderer { public constructor(targetLanguage: TargetLanguage, renderContext: RenderContext) { @@ -251,7 +31,7 @@ export class CrystalRenderer extends ConvenienceRenderer { return camelNamingFunction; } - protected forbiddenNamesForGlobalNamespace(): string[] { + protected forbiddenNamesForGlobalNamespace(): readonly string[] { return keywords; } diff --git a/packages/quicktype-core/src/language/Crystal/constants.ts b/packages/quicktype-core/src/language/Crystal/constants.ts new file mode 100644 index 000000000..9f9f99c5b --- /dev/null +++ b/packages/quicktype-core/src/language/Crystal/constants.ts @@ -0,0 +1,138 @@ +export const keywords = [ + "Any", + "Array", + "Atomic", + "Bool", + "Channel", + "Char", + "Class", + "Enum", + "Enumerable", + "Event", + "Extern", + "Exception", + "File", + "Float", + "Float32", + "Float64", + "GC", + "GZip", + "Hash", + "HTML", + "HTTP", + "Int", + "Int128", + "Int16", + "Int32", + "Int64", + "Int8", + "Iterable", + "Link", + "Logger", + "Math", + "Mutex", + "Nil", + "Number", + "JSON", + "IO", + "Object", + "Pointer", + "Proc", + "Process", + "Range", + "Random", + "Regex", + "Reference", + "Set", + "Signal", + "Slice", + "Spec", + "StaticArray", + "String", + "Struct", + "Symbol", + "System", + "TCPServer", + "TCPSocket", + "Socket", + "Tempfile", + "Termios", + "Time", + "Tuple", + "ThreadLocal", + "UDPSocket", + "UInt128", + "UInt16", + "UInt32", + "UInt64", + "UInt8", + "Union", + "UNIXServer", + "UNIXSocket", + "UUID", + "URI", + "VaList", + "Value", + "Void", + "WeakRef", + "XML", + "YAML", + "Zip", + "Zlib", + "abstract", + "alias", + "as", + "as?", + "asm", + "begin", + "break", + "case", + "class", + "def", + "do", + "else", + "elsif", + "end", + "ensure", + "enum", + "extend", + "false", + "for", + "fun", + "if", + "in", + "include", + "instance_sizeof", + "is_a?", + "lib", + "macro", + "module", + "next", + "nil", + "nil?", + "of", + "out", + "pointerof", + "private", + "protected", + "require", + "rescue", + "return", + "select", + "self", + "sizeof", + "struct", + "super", + "then", + "true", + "type", + "typeof", + "uninitialized", + "union", + "unless", + "until", + "when", + "while", + "with", + "yield" +] as const; diff --git a/packages/quicktype-core/src/language/Crystal/index.ts b/packages/quicktype-core/src/language/Crystal/index.ts new file mode 100644 index 000000000..d6b0ce5bc --- /dev/null +++ b/packages/quicktype-core/src/language/Crystal/index.ts @@ -0,0 +1,2 @@ +export { CrystalTargetLanguage } from "./language"; +export { CrystalRenderer } from "./CrystalRenderer"; diff --git a/packages/quicktype-core/src/language/Crystal/language.ts b/packages/quicktype-core/src/language/Crystal/language.ts new file mode 100644 index 000000000..ba8c81c67 --- /dev/null +++ b/packages/quicktype-core/src/language/Crystal/language.ts @@ -0,0 +1,24 @@ +import { type RenderContext } from "../../Renderer"; +import { type Option } from "../../RendererOptions"; +import { TargetLanguage } from "../../TargetLanguage"; +import { type FixMeOptionsAnyType } from "../../types"; + +import { CrystalRenderer } from "./CrystalRenderer"; + +export class CrystalTargetLanguage extends TargetLanguage { + protected makeRenderer(renderContext: RenderContext): CrystalRenderer { + return new CrystalRenderer(this, renderContext); + } + + public constructor() { + super("Crystal", ["crystal", "cr", "crystallang"], "cr"); + } + + protected get defaultIndentation(): string { + return " "; + } + + protected getOptions(): Array> { + return []; + } +} diff --git a/packages/quicktype-core/src/language/Crystal/utils.ts b/packages/quicktype-core/src/language/Crystal/utils.ts new file mode 100644 index 000000000..a4d95da50 --- /dev/null +++ b/packages/quicktype-core/src/language/Crystal/utils.ts @@ -0,0 +1,65 @@ +import { funPrefixNamer } from "../../Naming"; +import { + allLowerWordStyle, + combineWords, + escapeNonPrintableMapper, + firstUpperWordStyle, + intToHex, + isAscii, + isLetterOrUnderscore, + isLetterOrUnderscoreOrDigit, + isPrintable, + legalizeCharacters, + splitIntoWords, + utf32ConcatMap +} from "../../support/Strings"; + +function isAsciiLetterOrUnderscoreOrDigit(codePoint: number): boolean { + if (!isAscii(codePoint)) { + return false; + } + + return isLetterOrUnderscoreOrDigit(codePoint); +} + +function isAsciiLetterOrUnderscore(codePoint: number): boolean { + if (!isAscii(codePoint)) { + return false; + } + + return isLetterOrUnderscore(codePoint); +} + +const legalizeName = legalizeCharacters(isAsciiLetterOrUnderscoreOrDigit); + +function crystalStyle(original: string, isSnakeCase: boolean): string { + const words = splitIntoWords(original); + + const wordStyle = isSnakeCase ? allLowerWordStyle : firstUpperWordStyle; + + const combined = combineWords( + words, + legalizeName, + wordStyle, + wordStyle, + wordStyle, + wordStyle, + isSnakeCase ? "_" : "", + isAsciiLetterOrUnderscore + ); + + return combined === "_" ? "_underscore" : combined; +} + +export const snakeNamingFunction = funPrefixNamer("default", (original: string) => crystalStyle(original, true)); +export const camelNamingFunction = funPrefixNamer("camel", (original: string) => crystalStyle(original, false)); + +function standardUnicodeCrystalEscape(codePoint: number): string { + if (codePoint <= 0xffff) { + return "\\u{" + intToHex(codePoint, 4) + "}"; + } else { + return "\\u{" + intToHex(codePoint, 6) + "}"; + } +} + +export const crystalStringEscape = utf32ConcatMap(escapeNonPrintableMapper(isPrintable, standardUnicodeCrystalEscape)); diff --git a/packages/quicktype-core/src/language/Dart.ts b/packages/quicktype-core/src/language/Dart/DartRenderer.ts similarity index 80% rename from packages/quicktype-core/src/language/Dart.ts rename to packages/quicktype-core/src/language/Dart/DartRenderer.ts index d8d471ec9..75508c7b8 100644 --- a/packages/quicktype-core/src/language/Dart.ts +++ b/packages/quicktype-core/src/language/Dart/DartRenderer.ts @@ -1,218 +1,18 @@ -import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../Annotation"; -import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../ConvenienceRenderer"; -import { DependencyName, type Name, type Namer, funPrefixNamer } from "../Naming"; -import { type RenderContext } from "../Renderer"; -import { BooleanOption, type Option, type OptionValues, StringOption, getOptionValues } from "../RendererOptions"; -import { type Sourcelike, maybeAnnotated, modifySource } from "../Source"; -import { - allLowerWordStyle, - allUpperWordStyle, - combineWords, - decapitalize, - escapeNonPrintableMapper, - firstUpperWordStyle, - isAscii, - isDigit, - isLetter, - isPrintable, - snakeCase, - splitIntoWords, - standardUnicodeHexEscape, - utf16ConcatMap, - utf16LegalizeCharacters -} from "../support/Strings"; -import { defined } from "../support/Support"; -import { TargetLanguage } from "../TargetLanguage"; -import { - type ClassProperty, - type ClassType, - EnumType, - type PrimitiveStringTypeKind, - type TransformedStringTypeKind, - type Type, - type UnionType -} from "../Type"; -import { type StringTypeMapping } from "../TypeBuilder"; -import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../types"; -import { directlyReachableSingleNamedType, matchType, nullableFromUnion } from "../TypeUtils"; - -export const dartOptions = { - nullSafety: new BooleanOption("null-safety", "Null Safety", true), - justTypes: new BooleanOption("just-types", "Types only", false), - codersInClass: new BooleanOption("coders-in-class", "Put encoder & decoder in Class", false), - methodNamesWithMap: new BooleanOption("from-map", "Use method names fromMap() & toMap()", false, "secondary"), - requiredProperties: new BooleanOption("required-props", "Make all properties required", false), - finalProperties: new BooleanOption("final-props", "Make all properties final", false), - generateCopyWith: new BooleanOption("copy-with", "Generate CopyWith method", false), - useFreezed: new BooleanOption( - "use-freezed", - "Generate class definitions with @freezed compatibility", - false, - "secondary" - ), - useHive: new BooleanOption("use-hive", "Generate annotations for Hive type adapters", false, "secondary"), - useJsonAnnotation: new BooleanOption( - "use-json-annotation", - "Generate annotations for json_serializable", - false, - "secondary" - ), - partName: new StringOption("part-name", "Use this name in `part` directive", "NAME", "", "secondary") -}; - -export class DartTargetLanguage extends TargetLanguage { - public constructor() { - super("Dart", ["dart"], "dart"); - } - - protected getOptions(): Array> { - return [ - dartOptions.nullSafety, - dartOptions.justTypes, - dartOptions.codersInClass, - dartOptions.methodNamesWithMap, - dartOptions.requiredProperties, - dartOptions.finalProperties, - dartOptions.generateCopyWith, - dartOptions.useFreezed, - dartOptions.useHive, - dartOptions.useJsonAnnotation, - dartOptions.partName - ]; - } - - public get supportsUnionsWithBothNumberTypes(): boolean { - return true; - } - - public get stringTypeMapping(): StringTypeMapping { - const mapping: Map = new Map(); - mapping.set("date", "date"); - mapping.set("date-time", "date-time"); - return mapping; - } - - protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): DartRenderer { - const options = getOptionValues(dartOptions, untypedOptionValues); - return new DartRenderer(this, renderContext, options); - } -} - -const keywords = [ - "abstract", - "do", - "import", - "super", - "as", - "dynamic", - "in", - "switch", - "assert", - "else", - "interface", - "sync*", - "async", - "enum", - "is", - "this", - "async*", - "export", - "library", - "throw", - "await", - "external", - "mixin", - "true", - "break", - "extends", - "new", - "try", - "case", - "factory", - "null", - "typedef", - "catch", - "false", - "operator", - "var", - "class", - "final", - "part", - "void", - "const", - "finally", - "rethrow", - "while", - "continue", - "for", - "return", - "with", - "covariant", - "get", - "set", - "yield", - "default", - "if", - "static", - "yield*", - "deferred", - "implements", - "int", - "double", - "bool", - "Map", - "List", - "String", - "File", - "fromJson", - "toJson", - "fromMap", - "toMap" -]; - -const typeNamingFunction = funPrefixNamer("types", n => dartNameStyle(true, false, n)); -const propertyNamingFunction = funPrefixNamer("properties", n => dartNameStyle(false, false, n)); -const enumCaseNamingFunction = funPrefixNamer("enum-cases", n => dartNameStyle(true, true, n)); - -// Escape the dollar sign, which is used in string interpolation -const stringEscape = utf16ConcatMap( - escapeNonPrintableMapper(cp => isPrintable(cp) && cp !== 0x24, standardUnicodeHexEscape) -); - -function isStartCharacter(codePoint: number): boolean { - if (codePoint === 0x5f) return false; // underscore - return isAscii(codePoint) && isLetter(codePoint); -} - -function isPartCharacter(codePoint: number): boolean { - return isStartCharacter(codePoint) || (isAscii(codePoint) && isDigit(codePoint)); -} - -const legalizeName = utf16LegalizeCharacters(isPartCharacter); - -// FIXME: Handle acronyms consistently. In particular, that means that -// we have to use namers to produce the getter and setter names - we can't -// just capitalize and concatenate. -// https://stackoverflow.com/questions/8277355/naming-convention-for-upper-case-abbreviations -function dartNameStyle(startWithUpper: boolean, upperUnderscore: boolean, original: string): string { - const words = splitIntoWords(original); - const firstWordStyle = upperUnderscore - ? allUpperWordStyle - : startWithUpper - ? firstUpperWordStyle - : allLowerWordStyle; - const restWordStyle = upperUnderscore ? allUpperWordStyle : firstUpperWordStyle; - return combineWords( - words, - legalizeName, - firstWordStyle, - restWordStyle, - firstWordStyle, - restWordStyle, - upperUnderscore ? "_" : "", - isStartCharacter - ); -} +import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../../Annotation"; +import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../../ConvenienceRenderer"; +import { DependencyName, type Name, type Namer } from "../../Naming"; +import { type RenderContext } from "../../Renderer"; +import { type OptionValues } from "../../RendererOptions"; +import { type Sourcelike, maybeAnnotated, modifySource } from "../../Source"; +import { decapitalize, snakeCase } from "../../support/Strings"; +import { defined } from "../../support/Support"; +import { type TargetLanguage } from "../../TargetLanguage"; +import { type ClassProperty, type ClassType, EnumType, type Type, type UnionType } from "../../Type"; +import { directlyReachableSingleNamedType, matchType, nullableFromUnion } from "../../TypeUtils"; + +import { keywords } from "./constants"; +import { type dartOptions } from "./language"; +import { enumCaseNamingFunction, propertyNamingFunction, stringEscape, typeNamingFunction } from "./utils"; interface TopLevelDependents { decoder: Name; @@ -240,7 +40,7 @@ export class DartRenderer extends ConvenienceRenderer { super(targetLanguage, renderContext); } - protected forbiddenNamesForGlobalNamespace(): string[] { + protected forbiddenNamesForGlobalNamespace(): readonly string[] { return keywords; } @@ -879,15 +679,15 @@ export class DartRenderer extends ConvenienceRenderer { protected emitEnumValues(): void { this.ensureBlankLine(); this.emitMultiline(`class EnumValues { - Map map; - late Map reverseMap; + Map map; + late Map reverseMap; - EnumValues(this.map); + EnumValues(this.map); - Map get reverse { - reverseMap = map.map((k, v) => MapEntry(v, k)); - return reverseMap; - } + Map get reverse { + reverseMap = map.map((k, v) => MapEntry(v, k)); + return reverseMap; + } }`); } diff --git a/packages/quicktype-core/src/language/Dart/constants.ts b/packages/quicktype-core/src/language/Dart/constants.ts new file mode 100644 index 000000000..1251a6d8e --- /dev/null +++ b/packages/quicktype-core/src/language/Dart/constants.ts @@ -0,0 +1,71 @@ +export const keywords = [ + "abstract", + "do", + "import", + "super", + "as", + "dynamic", + "in", + "switch", + "assert", + "else", + "interface", + "sync*", + "async", + "enum", + "is", + "this", + "async*", + "export", + "library", + "throw", + "await", + "external", + "mixin", + "true", + "break", + "extends", + "new", + "try", + "case", + "factory", + "null", + "typedef", + "catch", + "false", + "operator", + "var", + "class", + "final", + "part", + "void", + "const", + "finally", + "rethrow", + "while", + "continue", + "for", + "return", + "with", + "covariant", + "get", + "set", + "yield", + "default", + "if", + "static", + "yield*", + "deferred", + "implements", + "int", + "double", + "bool", + "Map", + "List", + "String", + "File", + "fromJson", + "toJson", + "fromMap", + "toMap" +] as const; diff --git a/packages/quicktype-core/src/language/Dart/index.ts b/packages/quicktype-core/src/language/Dart/index.ts new file mode 100644 index 000000000..3820d5ad9 --- /dev/null +++ b/packages/quicktype-core/src/language/Dart/index.ts @@ -0,0 +1,2 @@ +export { DartTargetLanguage, dartOptions } from "./language"; +export { DartRenderer } from "./DartRenderer"; diff --git a/packages/quicktype-core/src/language/Dart/language.ts b/packages/quicktype-core/src/language/Dart/language.ts new file mode 100644 index 000000000..4598079f9 --- /dev/null +++ b/packages/quicktype-core/src/language/Dart/language.ts @@ -0,0 +1,70 @@ +import { type RenderContext } from "../../Renderer"; +import { BooleanOption, type Option, StringOption, getOptionValues } from "../../RendererOptions"; +import { TargetLanguage } from "../../TargetLanguage"; +import { type PrimitiveStringTypeKind, type TransformedStringTypeKind } from "../../Type"; +import { type StringTypeMapping } from "../../TypeBuilder"; +import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../../types"; + +import { DartRenderer } from "./DartRenderer"; + +export const dartOptions = { + nullSafety: new BooleanOption("null-safety", "Null Safety", true), + justTypes: new BooleanOption("just-types", "Types only", false), + codersInClass: new BooleanOption("coders-in-class", "Put encoder & decoder in Class", false), + methodNamesWithMap: new BooleanOption("from-map", "Use method names fromMap() & toMap()", false, "secondary"), + requiredProperties: new BooleanOption("required-props", "Make all properties required", false), + finalProperties: new BooleanOption("final-props", "Make all properties final", false), + generateCopyWith: new BooleanOption("copy-with", "Generate CopyWith method", false), + useFreezed: new BooleanOption( + "use-freezed", + "Generate class definitions with @freezed compatibility", + false, + "secondary" + ), + useHive: new BooleanOption("use-hive", "Generate annotations for Hive type adapters", false, "secondary"), + useJsonAnnotation: new BooleanOption( + "use-json-annotation", + "Generate annotations for json_serializable", + false, + "secondary" + ), + partName: new StringOption("part-name", "Use this name in `part` directive", "NAME", "", "secondary") +}; + +export class DartTargetLanguage extends TargetLanguage { + public constructor() { + super("Dart", ["dart"], "dart"); + } + + protected getOptions(): Array> { + return [ + dartOptions.nullSafety, + dartOptions.justTypes, + dartOptions.codersInClass, + dartOptions.methodNamesWithMap, + dartOptions.requiredProperties, + dartOptions.finalProperties, + dartOptions.generateCopyWith, + dartOptions.useFreezed, + dartOptions.useHive, + dartOptions.useJsonAnnotation, + dartOptions.partName + ]; + } + + public get supportsUnionsWithBothNumberTypes(): boolean { + return true; + } + + public get stringTypeMapping(): StringTypeMapping { + const mapping: Map = new Map(); + mapping.set("date", "date"); + mapping.set("date-time", "date-time"); + return mapping; + } + + protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): DartRenderer { + const options = getOptionValues(dartOptions, untypedOptionValues); + return new DartRenderer(this, renderContext, options); + } +} diff --git a/packages/quicktype-core/src/language/Dart/utils.ts b/packages/quicktype-core/src/language/Dart/utils.ts new file mode 100644 index 000000000..2fb4a783d --- /dev/null +++ b/packages/quicktype-core/src/language/Dart/utils.ts @@ -0,0 +1,60 @@ +import { funPrefixNamer } from "../../Naming"; +import { + allLowerWordStyle, + allUpperWordStyle, + combineWords, + escapeNonPrintableMapper, + firstUpperWordStyle, + isAscii, + isDigit, + isLetter, + isPrintable, + splitIntoWords, + standardUnicodeHexEscape, + utf16ConcatMap, + utf16LegalizeCharacters +} from "../../support/Strings"; + +export const typeNamingFunction = funPrefixNamer("types", n => dartNameStyle(true, false, n)); +export const propertyNamingFunction = funPrefixNamer("properties", n => dartNameStyle(false, false, n)); +export const enumCaseNamingFunction = funPrefixNamer("enum-cases", n => dartNameStyle(true, true, n)); + +// Escape the dollar sign, which is used in string interpolation +export const stringEscape = utf16ConcatMap( + escapeNonPrintableMapper(cp => isPrintable(cp) && cp !== 0x24, standardUnicodeHexEscape) +); + +function isStartCharacter(codePoint: number): boolean { + if (codePoint === 0x5f) return false; // underscore + return isAscii(codePoint) && isLetter(codePoint); +} + +function isPartCharacter(codePoint: number): boolean { + return isStartCharacter(codePoint) || (isAscii(codePoint) && isDigit(codePoint)); +} + +const legalizeName = utf16LegalizeCharacters(isPartCharacter); + +// FIXME: Handle acronyms consistently. In particular, that means that +// we have to use namers to produce the getter and setter names - we can't +// just capitalize and concatenate. +// https://stackoverflow.com/questions/8277355/naming-convention-for-upper-case-abbreviations +export function dartNameStyle(startWithUpper: boolean, upperUnderscore: boolean, original: string): string { + const words = splitIntoWords(original); + const firstWordStyle = upperUnderscore + ? allUpperWordStyle + : startWithUpper + ? firstUpperWordStyle + : allLowerWordStyle; + const restWordStyle = upperUnderscore ? allUpperWordStyle : firstUpperWordStyle; + return combineWords( + words, + legalizeName, + firstWordStyle, + restWordStyle, + firstWordStyle, + restWordStyle, + upperUnderscore ? "_" : "", + isStartCharacter + ); +} diff --git a/packages/quicktype-core/src/language/Elixir.ts b/packages/quicktype-core/src/language/Elixir/ElixirRenderer.ts similarity index 85% rename from packages/quicktype-core/src/language/Elixir.ts rename to packages/quicktype-core/src/language/Elixir/ElixirRenderer.ts index 3762dae7e..3665836a5 100644 --- a/packages/quicktype-core/src/language/Elixir.ts +++ b/packages/quicktype-core/src/language/Elixir/ElixirRenderer.ts @@ -1,214 +1,22 @@ -import * as unicode from "unicode-properties"; - -import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../ConvenienceRenderer"; -import { type Name, Namer } from "../Naming"; -import { type RenderContext } from "../Renderer"; -import { BooleanOption, type Option, type OptionValues, StringOption, getOptionValues } from "../RendererOptions"; -import { type Sourcelike } from "../Source"; +import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../../ConvenienceRenderer"; +import { type Name, Namer } from "../../Naming"; +import { type RenderContext } from "../../Renderer"; +import { type OptionValues } from "../../RendererOptions"; +import { type Sourcelike } from "../../Source"; +import { type TargetLanguage } from "../../TargetLanguage"; +import { ArrayType, ClassType, EnumType, MapType, PrimitiveType, type Type, UnionType } from "../../Type"; +import { matchType, nullableFromUnion } from "../../TypeUtils"; + +import { forbiddenModuleNames, reservedWords } from "./constants"; +import { type elixirOptions } from "./language"; import { - allLowerWordStyle, - allUpperWordStyle, - combineWords, - escapeNonPrintableMapper, - firstUpperWordStyle, - intToHex, - isLetterOrUnderscore, - isPrintable, - legalizeCharacters, - splitIntoWords, - utf32ConcatMap -} from "../support/Strings"; -import { TargetLanguage } from "../TargetLanguage"; -import { ArrayType, ClassType, EnumType, MapType, PrimitiveType, type Type, UnionType } from "../Type"; -import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../types"; -import { matchType, nullableFromUnion } from "../TypeUtils"; - -const forbiddenModuleNames = [ - "Access", - "Agent", - "Any", - "Application", - "ArgumentError", - "ArithmeticError", - "Atom", - "BadArityError", - "BadBooleanError", - "BadFunctionError", - "BadMapError", - "BadStructError", - "Base", - "Behaviour", - "Bitwise", - "Calendar", - "CaseClauseError", - "Code", - "Collectable", - "CondClauseError", - "Config", - "Date", - "DateTime", - "Dict", - "DynamicSupervisor", - "Enum", - "ErlangError", - "Exception", - "File", - "Float", - "Function", - "FunctionClauseError", - "GenEvent", - "GenServer", - "HashDict", - "HashSet", - "IO", - "Inspect", - "Integer", - "Kernel", - "KeyError", - "Keyword", - "List", - "Macro", - "Map", - "MapSet", - "MatchError", - "Module", - "Node", - "OptionParser", - "Path", - "Port", - "Process", - "Protocol", - "Range", - "Record", - "Regex", - "Registry", - "RuntimeError", - "Set", - "Stream", - "String", - "StringIO", - "Supervisor", - "SyntaxError", - "System", - "SystemLimitError", - "Task", - "Time", - "TokenMissingError", - "Tuple", - "URI", - "UndefinedFunctionError", - "UnicodeConversionError", - "Version", - "WithClauseError" -]; -const reservedWords = [ - "def", - "defmodule", - "use", - "import", - "alias", - "true", - "false", - "nil", - "when", - "and", - "or", - "not", - "in", - "fn", - "do", - "end", - "catch", - "rescue", - "after", - "else" -]; - -function unicodeEscape(codePoint: number): string { - return `\\u{${intToHex(codePoint, 0)}}`; -} - -function capitalizeFirstLetter(str: string): string { - return str.charAt(0).toUpperCase() + str.slice(1); -} - -const stringEscape = utf32ConcatMap(escapeNonPrintableMapper(isPrintable, unicodeEscape)); - -function escapeDoubleQuotes(str: string): string { - return str.replace(/"/g, '\\"'); -} - -function escapeNewLines(str: string): string { - return str.replace(/\n/g, "\\n"); -} - -export const elixirOptions = { - justTypes: new BooleanOption("just-types", "Plain types only", false), - namespace: new StringOption("namespace", "Specify a module namespace", "NAME", "") -}; - -export class ElixirTargetLanguage extends TargetLanguage { - public constructor() { - super("Elixir", ["elixir"], "ex"); - } - - protected getOptions(): Array> { - return [elixirOptions.justTypes, elixirOptions.namespace]; - } - - public get supportsOptionalClassProperties(): boolean { - return true; - } - - protected get defaultIndentation(): string { - return " "; - } - - protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): ElixirRenderer { - return new ElixirRenderer(this, renderContext, getOptionValues(elixirOptions, untypedOptionValues)); - } -} - -const isStartCharacter = isLetterOrUnderscore; - -function isPartCharacter(utf16Unit: number): boolean { - const category: string = unicode.getCategory(utf16Unit); - return ["Nd", "Pc", "Mn", "Mc"].includes(category) || isStartCharacter(utf16Unit); -} - -const legalizeName = legalizeCharacters(isPartCharacter); - -function simpleNameStyle(original: string, uppercase: boolean): string { - if (/^[0-9]+$/.test(original)) { - original = `${original}N`; - } - - const words = splitIntoWords(original); - return combineWords( - words, - legalizeName, - uppercase ? firstUpperWordStyle : allLowerWordStyle, - uppercase ? firstUpperWordStyle : allLowerWordStyle, - allUpperWordStyle, - allUpperWordStyle, - "", - isStartCharacter - ); -} - -function memberNameStyle(original: string): string { - const words = splitIntoWords(original); - return combineWords( - words, - legalizeName, - allLowerWordStyle, - allLowerWordStyle, - allLowerWordStyle, - allLowerWordStyle, - "_", - isStartCharacter - ); -} + capitalizeFirstLetter, + escapeDoubleQuotes, + escapeNewLines, + memberNameStyle, + simpleNameStyle, + stringEscape +} from "./utils"; export class ElixirRenderer extends ConvenienceRenderer { public constructor( @@ -236,7 +44,7 @@ export class ElixirRenderer extends ConvenienceRenderer { } protected forbiddenForObjectProperties(_c: ClassType, _classNamed: Name): ForbiddenWordsInfo { - return { names: reservedWords, includeGlobalForbidden: true }; + return { names: reservedWords as unknown as string[], includeGlobalForbidden: true }; } protected makeNamedTypeNamer(): Namer { @@ -890,8 +698,8 @@ export class ElixirRenderer extends ConvenienceRenderer { this.ensureBlankLine(); this.emitBlock("def from_json(json) do", () => { this.emitMultiline(`json - |> Jason.decode!() - |> from_map()`); + |> Jason.decode!() + |> from_map()`); }); this.ensureBlankLine(); this.emitBlock([`def to_map(${isEmpty ? "_" : ""}struct) do`], () => { @@ -907,8 +715,8 @@ export class ElixirRenderer extends ConvenienceRenderer { this.ensureBlankLine(); this.emitBlock("def to_json(struct) do", () => { this.emitMultiline(`struct - |> to_map() - |> Jason.encode!()`); + |> to_map() + |> Jason.encode!()`); }); }); } @@ -963,40 +771,40 @@ export class ElixirRenderer extends ConvenienceRenderer { this.emitMultiline(`def valid_atom?(value), do: value in @valid_enum_members def valid_atom_string?(value) do - try do - atom = String.to_existing_atom(value) - atom in @valid_enum_members - rescue - ArgumentError -> false - end + try do + atom = String.to_existing_atom(value) + atom in @valid_enum_members + rescue + ArgumentError -> false + end end def encode(value) do - if valid_atom?(value) do - Atom.to_string(value) - else - {:error, "Unexpected value when encoding atom: #{inspect(value)}"} - end + if valid_atom?(value) do + Atom.to_string(value) + else + {:error, "Unexpected value when encoding atom: #{inspect(value)}"} + end end def decode(value) do - if valid_atom_string?(value) do - String.to_existing_atom(value) - else - {:error, "Unexpected value when decoding atom: #{inspect(value)}"} - end + if valid_atom_string?(value) do + String.to_existing_atom(value) + else + {:error, "Unexpected value when decoding atom: #{inspect(value)}"} + end end def from_json(json) do - json - |> Jason.decode!() - |> decode() + json + |> Jason.decode!() + |> decode() end def to_json(data) do - data - |> encode() - |> Jason.encode!() + data + |> encode() + |> Jason.encode!() end`); }); } diff --git a/packages/quicktype-core/src/language/Elixir/constants.ts b/packages/quicktype-core/src/language/Elixir/constants.ts new file mode 100644 index 000000000..89866c9f5 --- /dev/null +++ b/packages/quicktype-core/src/language/Elixir/constants.ts @@ -0,0 +1,101 @@ +export const forbiddenModuleNames = [ + "Access", + "Agent", + "Any", + "Application", + "ArgumentError", + "ArithmeticError", + "Atom", + "BadArityError", + "BadBooleanError", + "BadFunctionError", + "BadMapError", + "BadStructError", + "Base", + "Behaviour", + "Bitwise", + "Calendar", + "CaseClauseError", + "Code", + "Collectable", + "CondClauseError", + "Config", + "Date", + "DateTime", + "Dict", + "DynamicSupervisor", + "Enum", + "ErlangError", + "Exception", + "File", + "Float", + "Function", + "FunctionClauseError", + "GenEvent", + "GenServer", + "HashDict", + "HashSet", + "IO", + "Inspect", + "Integer", + "Kernel", + "KeyError", + "Keyword", + "List", + "Macro", + "Map", + "MapSet", + "MatchError", + "Module", + "Node", + "OptionParser", + "Path", + "Port", + "Process", + "Protocol", + "Range", + "Record", + "Regex", + "Registry", + "RuntimeError", + "Set", + "Stream", + "String", + "StringIO", + "Supervisor", + "SyntaxError", + "System", + "SystemLimitError", + "Task", + "Time", + "TokenMissingError", + "Tuple", + "URI", + "UndefinedFunctionError", + "UnicodeConversionError", + "Version", + "WithClauseError" +] as const; + +export const reservedWords = [ + "def", + "defmodule", + "use", + "import", + "alias", + "true", + "false", + "nil", + "when", + "and", + "or", + "not", + "in", + "fn", + "do", + "end", + "catch", + "rescue", + "after", + "else" +] as const; diff --git a/packages/quicktype-core/src/language/Elixir/index.ts b/packages/quicktype-core/src/language/Elixir/index.ts new file mode 100644 index 000000000..59a6f6eb3 --- /dev/null +++ b/packages/quicktype-core/src/language/Elixir/index.ts @@ -0,0 +1,2 @@ +export { ElixirTargetLanguage, elixirOptions } from "./language"; +export { ElixirRenderer } from "./ElixirRenderer"; diff --git a/packages/quicktype-core/src/language/Elixir/language.ts b/packages/quicktype-core/src/language/Elixir/language.ts new file mode 100644 index 000000000..fceaa7063 --- /dev/null +++ b/packages/quicktype-core/src/language/Elixir/language.ts @@ -0,0 +1,33 @@ +import { type RenderContext } from "../../Renderer"; +import { BooleanOption, type Option, StringOption, getOptionValues } from "../../RendererOptions"; +import { TargetLanguage } from "../../TargetLanguage"; +import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../../types"; + +import { ElixirRenderer } from "./ElixirRenderer"; + +export const elixirOptions = { + justTypes: new BooleanOption("just-types", "Plain types only", false), + namespace: new StringOption("namespace", "Specify a module namespace", "NAME", "") +}; + +export class ElixirTargetLanguage extends TargetLanguage { + public constructor() { + super("Elixir", ["elixir"], "ex"); + } + + protected getOptions(): Array> { + return [elixirOptions.justTypes, elixirOptions.namespace]; + } + + public get supportsOptionalClassProperties(): boolean { + return true; + } + + protected get defaultIndentation(): string { + return " "; + } + + protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): ElixirRenderer { + return new ElixirRenderer(this, renderContext, getOptionValues(elixirOptions, untypedOptionValues)); + } +} diff --git a/packages/quicktype-core/src/language/Elixir/utils.ts b/packages/quicktype-core/src/language/Elixir/utils.ts new file mode 100644 index 000000000..4e81841fe --- /dev/null +++ b/packages/quicktype-core/src/language/Elixir/utils.ts @@ -0,0 +1,74 @@ +import unicode from "unicode-properties"; + +import { + allLowerWordStyle, + allUpperWordStyle, + combineWords, + escapeNonPrintableMapper, + firstUpperWordStyle, + intToHex, + isLetterOrUnderscore, + isPrintable, + legalizeCharacters, + splitIntoWords, + utf32ConcatMap +} from "../../support/Strings"; + +function unicodeEscape(codePoint: number): string { + return `\\u{${intToHex(codePoint, 0)}}`; +} + +export function capitalizeFirstLetter(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +export const stringEscape = utf32ConcatMap(escapeNonPrintableMapper(isPrintable, unicodeEscape)); + +export function escapeDoubleQuotes(str: string): string { + return str.replace(/"/g, '\\"'); +} + +export function escapeNewLines(str: string): string { + return str.replace(/\n/g, "\\n"); +} + +const isStartCharacter = isLetterOrUnderscore; + +function isPartCharacter(utf16Unit: number): boolean { + const category: string = unicode.getCategory(utf16Unit); + return ["Nd", "Pc", "Mn", "Mc"].includes(category) || isStartCharacter(utf16Unit); +} + +const legalizeName = legalizeCharacters(isPartCharacter); + +export function simpleNameStyle(original: string, uppercase: boolean): string { + if (/^[0-9]+$/.test(original)) { + original = `${original}N`; + } + + const words = splitIntoWords(original); + return combineWords( + words, + legalizeName, + uppercase ? firstUpperWordStyle : allLowerWordStyle, + uppercase ? firstUpperWordStyle : allLowerWordStyle, + allUpperWordStyle, + allUpperWordStyle, + "", + isStartCharacter + ); +} + +export function memberNameStyle(original: string): string { + const words = splitIntoWords(original); + return combineWords( + words, + legalizeName, + allLowerWordStyle, + allLowerWordStyle, + allLowerWordStyle, + allLowerWordStyle, + "_", + isStartCharacter + ); +} diff --git a/packages/quicktype-core/src/language/Elm.ts b/packages/quicktype-core/src/language/Elm/ElmRenderer.ts similarity index 84% rename from packages/quicktype-core/src/language/Elm.ts rename to packages/quicktype-core/src/language/Elm/ElmRenderer.ts index ced7837b4..8ae5a9d70 100644 --- a/packages/quicktype-core/src/language/Elm.ts +++ b/packages/quicktype-core/src/language/Elm/ElmRenderer.ts @@ -1,150 +1,20 @@ import { arrayIntercalate, mapContains } from "collection-utils"; -import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../Annotation"; -import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../ConvenienceRenderer"; -import { DependencyName, type Name, type Namer, funPrefixNamer } from "../Naming"; -import { type RenderContext } from "../Renderer"; -import { - BooleanOption, - EnumOption, - type Option, - type OptionValues, - StringOption, - getOptionValues -} from "../RendererOptions"; -import { type MultiWord, type Sourcelike, annotated, multiWord, parenIfNeeded, singleWord } from "../Source"; -import { - allLowerWordStyle, - allUpperWordStyle, - combineWords, - decapitalize, - firstUpperWordStyle, - isAscii, - isLetterOrUnderscore, - isLetterOrUnderscoreOrDigit, - legalizeCharacters, - splitIntoWords, - stringEscape -} from "../support/Strings"; -import { defined } from "../support/Support"; -import { TargetLanguage } from "../TargetLanguage"; -import { type ClassProperty, type ClassType, type EnumType, type Type, UnionType } from "../Type"; -import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../types"; -import { matchType, nullableFromUnion } from "../TypeUtils"; - -export const elmOptions = { - justTypes: new BooleanOption("just-types", "Plain types only", false), - useList: new EnumOption("array-type", "Use Array or List", [ - ["array", false], - ["list", true] - ]), - // FIXME: Do this via a configurable named eventually. - moduleName: new StringOption("module", "Generated module name", "NAME", "QuickType") -}; - -export class ElmTargetLanguage extends TargetLanguage { - public constructor() { - super("Elm", ["elm"], "elm"); - } - - protected getOptions(): Array> { - return [elmOptions.justTypes, elmOptions.moduleName, elmOptions.useList]; - } - - public get supportsOptionalClassProperties(): boolean { - return true; - } - - public get supportsUnionsWithBothNumberTypes(): boolean { - return true; - } - - protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): ElmRenderer { - return new ElmRenderer(this, renderContext, getOptionValues(elmOptions, untypedOptionValues)); - } -} - -const forbiddenNames = [ - "if", - "then", - "else", - "case", - "of", - "let", - "in", - "infix", - "type", - "module", - "where", - "import", - "exposing", - "as", - "port", - "int", - "float", - "bool", - "string", - "Jenc", - "Jdec", - "Jpipe", - "always", - "identity", - "Array", - "List", - "Dict", - "Maybe", - "map", - "toList", - "makeArrayEncoder", - "makeDictEncoder", - "makeNullableEncoder", - "Int", - "True", - "False", - "String", - "Float" -]; - -const legalizeName = legalizeCharacters(cp => isAscii(cp) && isLetterOrUnderscoreOrDigit(cp)); - -function elmNameStyle(original: string, upper: boolean): string { - const words = splitIntoWords(original); - return combineWords( - words, - legalizeName, - upper ? firstUpperWordStyle : allLowerWordStyle, - firstUpperWordStyle, - upper ? allUpperWordStyle : allLowerWordStyle, - allUpperWordStyle, - "", - isLetterOrUnderscore - ); -} - -const upperNamingFunction = funPrefixNamer("upper", n => elmNameStyle(n, true)); -const lowerNamingFunction = funPrefixNamer("lower", n => elmNameStyle(n, false)); - -interface RequiredOrOptional { - fallback: string; - reqOrOpt: string; -} - -function requiredOrOptional(p: ClassProperty): RequiredOrOptional { - function optional(fallback: string): RequiredOrOptional { - return { reqOrOpt: "Jpipe.optional", fallback }; - } - - const t = p.type; - if (p.isOptional || (t instanceof UnionType && nullableFromUnion(t) !== null)) { - return optional(" Nothing"); - } - - if (t.kind === "null") { - return optional(" ()"); - } - - return { reqOrOpt: "Jpipe.required", fallback: "" }; -} +import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../../Annotation"; +import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../../ConvenienceRenderer"; +import { DependencyName, type Name, type Namer } from "../../Naming"; +import { type RenderContext } from "../../Renderer"; +import { type OptionValues } from "../../RendererOptions"; +import { type MultiWord, type Sourcelike, annotated, multiWord, parenIfNeeded, singleWord } from "../../Source"; +import { decapitalize, stringEscape } from "../../support/Strings"; +import { defined } from "../../support/Support"; +import { type TargetLanguage } from "../../TargetLanguage"; +import { type ClassProperty, type ClassType, type EnumType, type Type, type UnionType } from "../../Type"; +import { matchType, nullableFromUnion } from "../../TypeUtils"; + +import { forbiddenNames } from "./constants"; +import { type elmOptions } from "./language"; +import { lowerNamingFunction, requiredOrOptional, upperNamingFunction } from "./utils"; interface TopLevelDependent { decoder?: Name; @@ -169,7 +39,7 @@ export class ElmRenderer extends ConvenienceRenderer { super(targetLanguage, renderContext); } - protected forbiddenNamesForGlobalNamespace(): string[] { + protected forbiddenNamesForGlobalNamespace(): readonly string[] { return forbiddenNames; } @@ -690,12 +560,12 @@ import Dict exposing (Dict, map, toList)`); this.ensureBlankLine(); this.emitMultiline(`makeDictEncoder : (a -> Jenc.Value) -> Dict String a -> Jenc.Value makeDictEncoder f dict = - Jenc.object (toList (Dict.map (\\k -> f) dict)) + Jenc.object (toList (Dict.map (\\k -> f) dict)) makeNullableEncoder : (a -> Jenc.Value) -> Maybe a -> Jenc.Value makeNullableEncoder f m = - case m of - Just x -> f x - Nothing -> Jenc.null`); + case m of + Just x -> f x + Nothing -> Jenc.null`); } } diff --git a/packages/quicktype-core/src/language/Elm/constants.ts b/packages/quicktype-core/src/language/Elm/constants.ts new file mode 100644 index 000000000..e7a331f86 --- /dev/null +++ b/packages/quicktype-core/src/language/Elm/constants.ts @@ -0,0 +1,40 @@ +export const forbiddenNames = [ + "if", + "then", + "else", + "case", + "of", + "let", + "in", + "infix", + "type", + "module", + "where", + "import", + "exposing", + "as", + "port", + "int", + "float", + "bool", + "string", + "Jenc", + "Jdec", + "Jpipe", + "always", + "identity", + "Array", + "List", + "Dict", + "Maybe", + "map", + "toList", + "makeArrayEncoder", + "makeDictEncoder", + "makeNullableEncoder", + "Int", + "True", + "False", + "String", + "Float" +] as const; diff --git a/packages/quicktype-core/src/language/Elm/index.ts b/packages/quicktype-core/src/language/Elm/index.ts new file mode 100644 index 000000000..9dcf263d0 --- /dev/null +++ b/packages/quicktype-core/src/language/Elm/index.ts @@ -0,0 +1,2 @@ +export { ElmTargetLanguage, elmOptions } from "./language"; +export { ElmRenderer } from "./ElmRenderer"; diff --git a/packages/quicktype-core/src/language/Elm/language.ts b/packages/quicktype-core/src/language/Elm/language.ts new file mode 100644 index 000000000..6a193fbae --- /dev/null +++ b/packages/quicktype-core/src/language/Elm/language.ts @@ -0,0 +1,38 @@ +import { type RenderContext } from "../../Renderer"; +import { BooleanOption, EnumOption, type Option, StringOption, getOptionValues } from "../../RendererOptions"; +import { TargetLanguage } from "../../TargetLanguage"; +import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../../types"; + +import { ElmRenderer } from "./ElmRenderer"; + +export const elmOptions = { + justTypes: new BooleanOption("just-types", "Plain types only", false), + useList: new EnumOption("array-type", "Use Array or List", [ + ["array", false], + ["list", true] + ]), + // FIXME: Do this via a configurable named eventually. + moduleName: new StringOption("module", "Generated module name", "NAME", "QuickType") +}; + +export class ElmTargetLanguage extends TargetLanguage { + public constructor() { + super("Elm", ["elm"], "elm"); + } + + protected getOptions(): Array> { + return [elmOptions.justTypes, elmOptions.moduleName, elmOptions.useList]; + } + + public get supportsOptionalClassProperties(): boolean { + return true; + } + + public get supportsUnionsWithBothNumberTypes(): boolean { + return true; + } + + protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): ElmRenderer { + return new ElmRenderer(this, renderContext, getOptionValues(elmOptions, untypedOptionValues)); + } +} diff --git a/packages/quicktype-core/src/language/Elm/utils.ts b/packages/quicktype-core/src/language/Elm/utils.ts new file mode 100644 index 000000000..2b09265cf --- /dev/null +++ b/packages/quicktype-core/src/language/Elm/utils.ts @@ -0,0 +1,55 @@ +import { funPrefixNamer } from "../../Naming"; +import { + allLowerWordStyle, + allUpperWordStyle, + combineWords, + firstUpperWordStyle, + isAscii, + isLetterOrUnderscore, + isLetterOrUnderscoreOrDigit, + legalizeCharacters, + splitIntoWords +} from "../../support/Strings"; +import { type ClassProperty, UnionType } from "../../Type"; +import { nullableFromUnion } from "../../TypeUtils"; + +const legalizeName = legalizeCharacters(cp => isAscii(cp) && isLetterOrUnderscoreOrDigit(cp)); + +function elmNameStyle(original: string, upper: boolean): string { + const words = splitIntoWords(original); + return combineWords( + words, + legalizeName, + upper ? firstUpperWordStyle : allLowerWordStyle, + firstUpperWordStyle, + upper ? allUpperWordStyle : allLowerWordStyle, + allUpperWordStyle, + "", + isLetterOrUnderscore + ); +} + +export const upperNamingFunction = funPrefixNamer("upper", n => elmNameStyle(n, true)); +export const lowerNamingFunction = funPrefixNamer("lower", n => elmNameStyle(n, false)); + +interface RequiredOrOptional { + fallback: string; + reqOrOpt: string; +} + +export function requiredOrOptional(p: ClassProperty): RequiredOrOptional { + function optional(fallback: string): RequiredOrOptional { + return { reqOrOpt: "Jpipe.optional", fallback }; + } + + const t = p.type; + if (p.isOptional || (t instanceof UnionType && nullableFromUnion(t) !== null)) { + return optional(" Nothing"); + } + + if (t.kind === "null") { + return optional(" ()"); + } + + return { reqOrOpt: "Jpipe.required", fallback: "" }; +} diff --git a/packages/quicktype-core/src/language/Golang.ts b/packages/quicktype-core/src/language/Golang/GolangRenderer.ts similarity index 73% rename from packages/quicktype-core/src/language/Golang.ts rename to packages/quicktype-core/src/language/Golang/GolangRenderer.ts index 9a2081af6..708666452 100644 --- a/packages/quicktype-core/src/language/Golang.ts +++ b/packages/quicktype-core/src/language/Golang/GolangRenderer.ts @@ -1,111 +1,17 @@ -import { type PrimitiveStringTypeKind, type StringTypeMapping, type TransformedStringTypeKind } from ".."; -import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../Annotation"; -import { ConvenienceRenderer } from "../ConvenienceRenderer"; -import { DependencyName, type Name, type Namer, funPrefixNamer } from "../Naming"; -import { type RenderContext } from "../Renderer"; -import { BooleanOption, type Option, type OptionValues, StringOption, getOptionValues } from "../RendererOptions"; -import { type Sourcelike, maybeAnnotated, modifySource } from "../Source"; -import { - allUpperWordStyle, - camelCase, - combineWords, - firstUpperWordStyle, - isLetterOrUnderscore, - isLetterOrUnderscoreOrDigit, - legalizeCharacters, - splitIntoWords, - stringEscape -} from "../support/Strings"; -import { assert, defined } from "../support/Support"; -import { TargetLanguage } from "../TargetLanguage"; -import { type ClassProperty, type ClassType, type EnumType, type Type, type TypeKind, UnionType } from "../Type"; -import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../types"; -import { matchType, nullableFromUnion, removeNullFromUnion } from "../TypeUtils"; - -export const goOptions = { - justTypes: new BooleanOption("just-types", "Plain types only", false), - justTypesAndPackage: new BooleanOption("just-types-and-package", "Plain types with package only", false), - packageName: new StringOption("package", "Generated package name", "NAME", "main"), - multiFileOutput: new BooleanOption("multi-file-output", "Renders each top-level object in its own Go file", false), - fieldTags: new StringOption("field-tags", "list of tags which should be generated for fields", "TAGS", "json"), - omitEmpty: new BooleanOption( - "omit-empty", - 'If set, all non-required objects will be tagged with ",omitempty"', - false - ) -}; - -export class GoTargetLanguage extends TargetLanguage { - public constructor() { - super("Go", ["go", "golang"], "go"); - } - - protected getOptions(): Array> { - return [ - goOptions.justTypes, - goOptions.justTypesAndPackage, - goOptions.packageName, - goOptions.multiFileOutput, - goOptions.fieldTags, - goOptions.omitEmpty - ]; - } - - public get supportsUnionsWithBothNumberTypes(): boolean { - return true; - } - - public get stringTypeMapping(): StringTypeMapping { - const mapping: Map = new Map(); - mapping.set("date-time", "date-time"); - return mapping; - } - - public get supportsOptionalClassProperties(): boolean { - return true; - } - - protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): GoRenderer { - return new GoRenderer(this, renderContext, getOptionValues(goOptions, untypedOptionValues)); - } - - protected get defaultIndentation(): string { - return "\t"; - } -} - -const namingFunction = funPrefixNamer("namer", goNameStyle); - -const legalizeName = legalizeCharacters(isLetterOrUnderscoreOrDigit); - -function goNameStyle(original: string): string { - const words = splitIntoWords(original); - return combineWords( - words, - legalizeName, - firstUpperWordStyle, - firstUpperWordStyle, - allUpperWordStyle, - allUpperWordStyle, - "", - isLetterOrUnderscore - ); -} - -const primitiveValueTypeKinds: TypeKind[] = ["integer", "double", "bool", "string"]; -const compoundTypeKinds: TypeKind[] = ["array", "class", "map", "enum"]; - -function isValueType(t: Type): boolean { - const kind = t.kind; - return primitiveValueTypeKinds.includes(kind) || kind === "class" || kind === "enum" || kind === "date-time"; -} - -function canOmitEmpty(cp: ClassProperty, omitEmptyOption: boolean): boolean { - if (!cp.isOptional) return false; - if (omitEmptyOption) return true; - const t = cp.type; - return !["union", "null", "any"].includes(t.kind); -} +import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../../Annotation"; +import { ConvenienceRenderer } from "../../ConvenienceRenderer"; +import { DependencyName, type Name, type Namer } from "../../Naming"; +import { type RenderContext } from "../../Renderer"; +import { type OptionValues } from "../../RendererOptions"; +import { type Sourcelike, maybeAnnotated, modifySource } from "../../Source"; +import { camelCase, stringEscape } from "../../support/Strings"; +import { assert, defined } from "../../support/Support"; +import { type TargetLanguage } from "../../TargetLanguage"; +import { type ClassProperty, type ClassType, type EnumType, type Type, type TypeKind, UnionType } from "../../Type"; +import { matchType, nullableFromUnion, removeNullFromUnion } from "../../TypeUtils"; + +import { type goOptions } from "./language"; +import { canOmitEmpty, compoundTypeKinds, isValueType, namingFunction, primitiveValueTypeKinds } from "./utils"; export class GoRenderer extends ConvenienceRenderer { private readonly _topLevelUnmarshalNames = new Map(); @@ -495,117 +401,117 @@ export class GoRenderer extends ConvenienceRenderer { this.ensureBlankLine(); this .emitMultiline(`func unmarshalUnion(data []byte, pi **int64, pf **float64, pb **bool, ps **string, haveArray bool, pa interface{}, haveObject bool, pc interface{}, haveMap bool, pm interface{}, haveEnum bool, pe interface{}, nullable bool) (bool, error) { - if pi != nil { - *pi = nil - } - if pf != nil { - *pf = nil - } - if pb != nil { - *pb = nil - } - if ps != nil { - *ps = nil - } - - dec := json.NewDecoder(bytes.NewReader(data)) - dec.UseNumber() - tok, err := dec.Token() - if err != nil { - return false, err - } - - switch v := tok.(type) { - case json.Number: - if pi != nil { - i, err := v.Int64() - if err == nil { - *pi = &i - return false, nil - } - } - if pf != nil { - f, err := v.Float64() - if err == nil { - *pf = &f - return false, nil - } - return false, errors.New("Unparsable number") - } - return false, errors.New("Union does not contain number") - case float64: - return false, errors.New("Decoder should not return float64") - case bool: - if pb != nil { - *pb = &v - return false, nil - } - return false, errors.New("Union does not contain bool") - case string: - if haveEnum { - return false, json.Unmarshal(data, pe) - } - if ps != nil { - *ps = &v - return false, nil - } - return false, errors.New("Union does not contain string") - case nil: - if nullable { - return false, nil - } - return false, errors.New("Union does not contain null") - case json.Delim: - if v == '{' { - if haveObject { - return true, json.Unmarshal(data, pc) - } - if haveMap { - return false, json.Unmarshal(data, pm) - } - return false, errors.New("Union does not contain object") - } - if v == '[' { - if haveArray { - return false, json.Unmarshal(data, pa) - } - return false, errors.New("Union does not contain array") - } - return false, errors.New("Cannot handle delimiter") - } - return false, errors.New("Cannot unmarshal union") + if pi != nil { + *pi = nil + } + if pf != nil { + *pf = nil + } + if pb != nil { + *pb = nil + } + if ps != nil { + *ps = nil + } + + dec := json.NewDecoder(bytes.NewReader(data)) + dec.UseNumber() + tok, err := dec.Token() + if err != nil { + return false, err + } + + switch v := tok.(type) { + case json.Number: + if pi != nil { + i, err := v.Int64() + if err == nil { + *pi = &i + return false, nil + } + } + if pf != nil { + f, err := v.Float64() + if err == nil { + *pf = &f + return false, nil + } + return false, errors.New("Unparsable number") + } + return false, errors.New("Union does not contain number") + case float64: + return false, errors.New("Decoder should not return float64") + case bool: + if pb != nil { + *pb = &v + return false, nil + } + return false, errors.New("Union does not contain bool") + case string: + if haveEnum { + return false, json.Unmarshal(data, pe) + } + if ps != nil { + *ps = &v + return false, nil + } + return false, errors.New("Union does not contain string") + case nil: + if nullable { + return false, nil + } + return false, errors.New("Union does not contain null") + case json.Delim: + if v == '{' { + if haveObject { + return true, json.Unmarshal(data, pc) + } + if haveMap { + return false, json.Unmarshal(data, pm) + } + return false, errors.New("Union does not contain object") + } + if v == '[' { + if haveArray { + return false, json.Unmarshal(data, pa) + } + return false, errors.New("Union does not contain array") + } + return false, errors.New("Cannot handle delimiter") + } + return false, errors.New("Cannot unmarshal union") } func marshalUnion(pi *int64, pf *float64, pb *bool, ps *string, haveArray bool, pa interface{}, haveObject bool, pc interface{}, haveMap bool, pm interface{}, haveEnum bool, pe interface{}, nullable bool) ([]byte, error) { - if pi != nil { - return json.Marshal(*pi) - } - if pf != nil { - return json.Marshal(*pf) - } - if pb != nil { - return json.Marshal(*pb) - } - if ps != nil { - return json.Marshal(*ps) - } - if haveArray { - return json.Marshal(pa) - } - if haveObject { - return json.Marshal(pc) - } - if haveMap { - return json.Marshal(pm) - } - if haveEnum { - return json.Marshal(pe) - } - if nullable { - return json.Marshal(nil) - } - return nil, errors.New("Union must not be null") + if pi != nil { + return json.Marshal(*pi) + } + if pf != nil { + return json.Marshal(*pf) + } + if pb != nil { + return json.Marshal(*pb) + } + if ps != nil { + return json.Marshal(*ps) + } + if haveArray { + return json.Marshal(pa) + } + if haveObject { + return json.Marshal(pc) + } + if haveMap { + return json.Marshal(pm) + } + if haveEnum { + return json.Marshal(pe) + } + if nullable { + return json.Marshal(nil) + } + return nil, errors.New("Union must not be null") }`); this.endFile(); } diff --git a/packages/quicktype-core/src/language/Golang/index.ts b/packages/quicktype-core/src/language/Golang/index.ts new file mode 100644 index 000000000..04d294d43 --- /dev/null +++ b/packages/quicktype-core/src/language/Golang/index.ts @@ -0,0 +1,2 @@ +export { GoTargetLanguage, goOptions } from "./language"; +export { GoRenderer } from "./GolangRenderer"; diff --git a/packages/quicktype-core/src/language/Golang/language.ts b/packages/quicktype-core/src/language/Golang/language.ts new file mode 100644 index 000000000..f38edff15 --- /dev/null +++ b/packages/quicktype-core/src/language/Golang/language.ts @@ -0,0 +1,60 @@ +import { type RenderContext } from "../../Renderer"; +import { BooleanOption, type Option, StringOption, getOptionValues } from "../../RendererOptions"; +import { TargetLanguage } from "../../TargetLanguage"; +import { type PrimitiveStringTypeKind, type TransformedStringTypeKind } from "../../Type"; +import { type StringTypeMapping } from "../../TypeBuilder"; +import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../../types"; + +import { GoRenderer } from "./GolangRenderer"; + +export const goOptions = { + justTypes: new BooleanOption("just-types", "Plain types only", false), + justTypesAndPackage: new BooleanOption("just-types-and-package", "Plain types with package only", false), + packageName: new StringOption("package", "Generated package name", "NAME", "main"), + multiFileOutput: new BooleanOption("multi-file-output", "Renders each top-level object in its own Go file", false), + fieldTags: new StringOption("field-tags", "list of tags which should be generated for fields", "TAGS", "json"), + omitEmpty: new BooleanOption( + "omit-empty", + 'If set, all non-required objects will be tagged with ",omitempty"', + false + ) +}; + +export class GoTargetLanguage extends TargetLanguage { + public constructor() { + super("Go", ["go", "golang"], "go"); + } + + protected getOptions(): Array> { + return [ + goOptions.justTypes, + goOptions.justTypesAndPackage, + goOptions.packageName, + goOptions.multiFileOutput, + goOptions.fieldTags, + goOptions.omitEmpty + ]; + } + + public get supportsUnionsWithBothNumberTypes(): boolean { + return true; + } + + public get stringTypeMapping(): StringTypeMapping { + const mapping: Map = new Map(); + mapping.set("date-time", "date-time"); + return mapping; + } + + public get supportsOptionalClassProperties(): boolean { + return true; + } + + protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): GoRenderer { + return new GoRenderer(this, renderContext, getOptionValues(goOptions, untypedOptionValues)); + } + + protected get defaultIndentation(): string { + return "\t"; + } +} diff --git a/packages/quicktype-core/src/language/Golang/utils.ts b/packages/quicktype-core/src/language/Golang/utils.ts new file mode 100644 index 000000000..ce67443af --- /dev/null +++ b/packages/quicktype-core/src/language/Golang/utils.ts @@ -0,0 +1,44 @@ +import { funPrefixNamer } from "../../Naming"; +import { + allUpperWordStyle, + combineWords, + firstUpperWordStyle, + isLetterOrUnderscore, + isLetterOrUnderscoreOrDigit, + legalizeCharacters, + splitIntoWords +} from "../../support/Strings"; +import { type ClassProperty, type Type, type TypeKind } from "../../Type"; + +export const namingFunction = funPrefixNamer("namer", goNameStyle); + +const legalizeName = legalizeCharacters(isLetterOrUnderscoreOrDigit); + +function goNameStyle(original: string): string { + const words = splitIntoWords(original); + return combineWords( + words, + legalizeName, + firstUpperWordStyle, + firstUpperWordStyle, + allUpperWordStyle, + allUpperWordStyle, + "", + isLetterOrUnderscore + ); +} + +export const primitiveValueTypeKinds: TypeKind[] = ["integer", "double", "bool", "string"]; +export const compoundTypeKinds: TypeKind[] = ["array", "class", "map", "enum"]; + +export function isValueType(t: Type): boolean { + const kind = t.kind; + return primitiveValueTypeKinds.includes(kind) || kind === "class" || kind === "enum" || kind === "date-time"; +} + +export function canOmitEmpty(cp: ClassProperty, omitEmptyOption: boolean): boolean { + if (!cp.isOptional) return false; + if (omitEmptyOption) return true; + const t = cp.type; + return !["union", "null", "any"].includes(t.kind); +} diff --git a/packages/quicktype-core/src/language/Haskell.ts b/packages/quicktype-core/src/language/Haskell/HaskellRenderer.ts similarity index 82% rename from packages/quicktype-core/src/language/Haskell.ts rename to packages/quicktype-core/src/language/Haskell/HaskellRenderer.ts index 16cd16a35..b5a2143b4 100644 --- a/packages/quicktype-core/src/language/Haskell.ts +++ b/packages/quicktype-core/src/language/Haskell/HaskellRenderer.ts @@ -1,138 +1,18 @@ import { mapContains } from "collection-utils"; -import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../ConvenienceRenderer"; -import { type Name, type Namer, funPrefixNamer } from "../Naming"; -import { type RenderContext } from "../Renderer"; -import { - BooleanOption, - EnumOption, - type Option, - type OptionValues, - StringOption, - getOptionValues -} from "../RendererOptions"; -import { type MultiWord, type Sourcelike, multiWord, parenIfNeeded, singleWord } from "../Source"; -import { - allLowerWordStyle, - allUpperWordStyle, - combineWords, - firstUpperWordStyle, - isAscii, - isLetterOrUnderscore, - isLetterOrUnderscoreOrDigit, - legalizeCharacters, - splitIntoWords, - stringEscape -} from "../support/Strings"; -import { TargetLanguage } from "../TargetLanguage"; -import { type ClassProperty, type ClassType, type EnumType, type Type, type UnionType } from "../Type"; -import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../types"; -import { matchType, nullableFromUnion } from "../TypeUtils"; - -export const haskellOptions = { - justTypes: new BooleanOption("just-types", "Plain types only", false), - useList: new EnumOption("array-type", "Use Array or List", [ - ["array", false], - ["list", true] - ]), - moduleName: new StringOption("module", "Generated module name", "NAME", "QuickType") -}; - -export class HaskellTargetLanguage extends TargetLanguage { - public constructor() { - super("Haskell", ["haskell"], "haskell"); - } - - protected getOptions(): Array> { - return [haskellOptions.justTypes, haskellOptions.moduleName, haskellOptions.useList]; - } - - public get supportsOptionalClassProperties(): boolean { - return true; - } - - public get supportsUnionsWithBothNumberTypes(): boolean { - return true; - } - - protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): HaskellRenderer { - return new HaskellRenderer(this, renderContext, getOptionValues(haskellOptions, untypedOptionValues)); - } -} - -const forbiddenNames = [ - // reserved keywords - "as", - "case", - "class", - "data", - "default", - "deriving", - "do", - "else", - "family", - "forall", - "foreign", - "hiding", - "if", - "import", - "in", - "infix", - "infixl", - "infixr", - "instance", - "let", - "of", - "mdo", - "module", - "newtype", - "proc", - "qualified", - "rec", - "then", - "type", - "where", - // in Prelude keywords ... - "id", - "Array", - "HashMap", - "Map", - "Maybe", - "Bool", - "Int", - "True", - "False", - "Enum", - // Aeson types - "encode", - "decode", - "text", - "Text", - "Value", - "Object", - "Result", - "Series", - "Error" -]; - -const legalizeName = legalizeCharacters(cp => isAscii(cp) && isLetterOrUnderscoreOrDigit(cp)); - -function haskellNameStyle(original: string, upper: boolean): string { - const words = splitIntoWords(original); - return combineWords( - words, - legalizeName, - upper ? firstUpperWordStyle : allLowerWordStyle, - firstUpperWordStyle, - upper ? allUpperWordStyle : allLowerWordStyle, - allUpperWordStyle, - "", - isLetterOrUnderscore - ); -} - -const upperNamingFunction = funPrefixNamer("upper", n => haskellNameStyle(n, true)); -const lowerNamingFunction = funPrefixNamer("lower", n => haskellNameStyle(n, false)); +import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../../ConvenienceRenderer"; +import { type Name, type Namer } from "../../Naming"; +import { type RenderContext } from "../../Renderer"; +import { type OptionValues } from "../../RendererOptions"; +import { type MultiWord, type Sourcelike, multiWord, parenIfNeeded, singleWord } from "../../Source"; +import { stringEscape } from "../../support/Strings"; +import { type TargetLanguage } from "../../TargetLanguage"; +import { type ClassProperty, type ClassType, type EnumType, type Type, type UnionType } from "../../Type"; +import { matchType, nullableFromUnion } from "../../TypeUtils"; + +import { forbiddenNames } from "./constants"; +import { type haskellOptions } from "./language"; +import { lowerNamingFunction, upperNamingFunction } from "./utils"; export class HaskellRenderer extends ConvenienceRenderer { public constructor( @@ -143,7 +23,7 @@ export class HaskellRenderer extends ConvenienceRenderer { super(targetLanguage, renderContext); } - protected forbiddenNamesForGlobalNamespace(): string[] { + protected forbiddenNamesForGlobalNamespace(): readonly string[] { return forbiddenNames; } diff --git a/packages/quicktype-core/src/language/Haskell/constants.ts b/packages/quicktype-core/src/language/Haskell/constants.ts new file mode 100644 index 000000000..5e09e6702 --- /dev/null +++ b/packages/quicktype-core/src/language/Haskell/constants.ts @@ -0,0 +1,54 @@ +export const forbiddenNames = [ + // reserved keywords + "as", + "case", + "class", + "data", + "default", + "deriving", + "do", + "else", + "family", + "forall", + "foreign", + "hiding", + "if", + "import", + "in", + "infix", + "infixl", + "infixr", + "instance", + "let", + "of", + "mdo", + "module", + "newtype", + "proc", + "qualified", + "rec", + "then", + "type", + "where", + // in Prelude keywords ... + "id", + "Array", + "HashMap", + "Map", + "Maybe", + "Bool", + "Int", + "True", + "False", + "Enum", + // Aeson types + "encode", + "decode", + "text", + "Text", + "Value", + "Object", + "Result", + "Series", + "Error" +] as const; diff --git a/packages/quicktype-core/src/language/Haskell/index.ts b/packages/quicktype-core/src/language/Haskell/index.ts new file mode 100644 index 000000000..9658076f0 --- /dev/null +++ b/packages/quicktype-core/src/language/Haskell/index.ts @@ -0,0 +1,2 @@ +export { HaskellTargetLanguage, haskellOptions } from "./language"; +export { HaskellRenderer } from "./HaskellRenderer"; diff --git a/packages/quicktype-core/src/language/Haskell/language.ts b/packages/quicktype-core/src/language/Haskell/language.ts new file mode 100644 index 000000000..84ad167f3 --- /dev/null +++ b/packages/quicktype-core/src/language/Haskell/language.ts @@ -0,0 +1,37 @@ +import { type RenderContext } from "../../Renderer"; +import { BooleanOption, EnumOption, type Option, StringOption, getOptionValues } from "../../RendererOptions"; +import { TargetLanguage } from "../../TargetLanguage"; +import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../../types"; + +import { HaskellRenderer } from "./HaskellRenderer"; + +export const haskellOptions = { + justTypes: new BooleanOption("just-types", "Plain types only", false), + useList: new EnumOption("array-type", "Use Array or List", [ + ["array", false], + ["list", true] + ]), + moduleName: new StringOption("module", "Generated module name", "NAME", "QuickType") +}; + +export class HaskellTargetLanguage extends TargetLanguage { + public constructor() { + super("Haskell", ["haskell"], "haskell"); + } + + protected getOptions(): Array> { + return [haskellOptions.justTypes, haskellOptions.moduleName, haskellOptions.useList]; + } + + public get supportsOptionalClassProperties(): boolean { + return true; + } + + public get supportsUnionsWithBothNumberTypes(): boolean { + return true; + } + + protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): HaskellRenderer { + return new HaskellRenderer(this, renderContext, getOptionValues(haskellOptions, untypedOptionValues)); + } +} diff --git a/packages/quicktype-core/src/language/Haskell/utils.ts b/packages/quicktype-core/src/language/Haskell/utils.ts new file mode 100644 index 000000000..3f4b1357f --- /dev/null +++ b/packages/quicktype-core/src/language/Haskell/utils.ts @@ -0,0 +1,31 @@ +import { funPrefixNamer } from "../../Naming"; +import { + allLowerWordStyle, + allUpperWordStyle, + combineWords, + firstUpperWordStyle, + isAscii, + isLetterOrUnderscore, + isLetterOrUnderscoreOrDigit, + legalizeCharacters, + splitIntoWords +} from "../../support/Strings"; + +const legalizeName = legalizeCharacters(cp => isAscii(cp) && isLetterOrUnderscoreOrDigit(cp)); + +function haskellNameStyle(original: string, upper: boolean): string { + const words = splitIntoWords(original); + return combineWords( + words, + legalizeName, + upper ? firstUpperWordStyle : allLowerWordStyle, + firstUpperWordStyle, + upper ? allUpperWordStyle : allLowerWordStyle, + allUpperWordStyle, + "", + isLetterOrUnderscore + ); +} + +export const upperNamingFunction = funPrefixNamer("upper", n => haskellNameStyle(n, true)); +export const lowerNamingFunction = funPrefixNamer("lower", n => haskellNameStyle(n, false)); diff --git a/packages/quicktype-core/src/language/JSONSchema.ts b/packages/quicktype-core/src/language/JSONSchema/JSONSchemaRenderer.ts similarity index 75% rename from packages/quicktype-core/src/language/JSONSchema.ts rename to packages/quicktype-core/src/language/JSONSchema/JSONSchemaRenderer.ts index 978906075..9b24306fa 100644 --- a/packages/quicktype-core/src/language/JSONSchema.ts +++ b/packages/quicktype-core/src/language/JSONSchema/JSONSchemaRenderer.ts @@ -1,73 +1,19 @@ import { iterableFirst, mapFirst } from "collection-utils"; -import { addDescriptionToSchema } from "../attributes/Description"; -import { ConvenienceRenderer } from "../ConvenienceRenderer"; -import { type Name, type Namer, funPrefixNamer } from "../Naming"; -import { type RenderContext } from "../Renderer"; -import { type Option } from "../RendererOptions"; -import { - allUpperWordStyle, - combineWords, - firstUpperWordStyle, - legalizeCharacters, - splitIntoWords -} from "../support/Strings"; -import { defined, panic } from "../support/Support"; -import { TargetLanguage } from "../TargetLanguage"; +import { addDescriptionToSchema } from "../../attributes/Description"; +import { ConvenienceRenderer } from "../../ConvenienceRenderer"; +import { type Name, type Namer } from "../../Naming"; +import { defined, panic } from "../../support/Support"; import { type EnumType, type ObjectType, type Type, type UnionType, transformedStringTypeTargetTypeKindsMap -} from "../Type"; -import { type StringTypeMapping, getNoStringTypeMapping } from "../TypeBuilder"; -import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../types"; -import { matchTypeExhaustive } from "../TypeUtils"; - -export class JSONSchemaTargetLanguage extends TargetLanguage { - public constructor() { - super("JSON Schema", ["schema", "json-schema"], "schema"); - } - - protected getOptions(): Array> { - return []; - } - - public get stringTypeMapping(): StringTypeMapping { - return getNoStringTypeMapping(); - } - - public get supportsOptionalClassProperties(): boolean { - return true; - } - - public get supportsFullObjectType(): boolean { - return true; - } +} from "../../Type"; +import { matchTypeExhaustive } from "../../TypeUtils"; - protected makeRenderer(renderContext: RenderContext, _untypedOptionValues: FixMeOptionsType): JSONSchemaRenderer { - return new JSONSchemaRenderer(this, renderContext); - } -} - -const namingFunction = funPrefixNamer("namer", jsonNameStyle); - -const legalizeName = legalizeCharacters(cp => cp >= 32 && cp < 128 && cp !== 0x2f /* slash */); - -function jsonNameStyle(original: string): string { - const words = splitIntoWords(original); - return combineWords( - words, - legalizeName, - firstUpperWordStyle, - firstUpperWordStyle, - allUpperWordStyle, - allUpperWordStyle, - "", - _ => true - ); -} +import { namingFunction } from "./utils"; interface Schema { // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/quicktype-core/src/language/JSONSchema/index.ts b/packages/quicktype-core/src/language/JSONSchema/index.ts new file mode 100644 index 000000000..6cb0b7958 --- /dev/null +++ b/packages/quicktype-core/src/language/JSONSchema/index.ts @@ -0,0 +1,2 @@ +export { JSONSchemaTargetLanguage } from "./language"; +export { JSONSchemaRenderer } from "./JSONSchemaRenderer"; diff --git a/packages/quicktype-core/src/language/JSONSchema/language.ts b/packages/quicktype-core/src/language/JSONSchema/language.ts new file mode 100644 index 000000000..7374537a1 --- /dev/null +++ b/packages/quicktype-core/src/language/JSONSchema/language.ts @@ -0,0 +1,33 @@ +import { type RenderContext } from "../../Renderer"; +import { type Option } from "../../RendererOptions"; +import { TargetLanguage } from "../../TargetLanguage"; +import { type StringTypeMapping, getNoStringTypeMapping } from "../../TypeBuilder"; +import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../../types"; + +import { JSONSchemaRenderer } from "./JSONSchemaRenderer"; + +export class JSONSchemaTargetLanguage extends TargetLanguage { + public constructor() { + super("JSON Schema", ["schema", "json-schema"], "schema"); + } + + protected getOptions(): Array> { + return []; + } + + public get stringTypeMapping(): StringTypeMapping { + return getNoStringTypeMapping(); + } + + public get supportsOptionalClassProperties(): boolean { + return true; + } + + public get supportsFullObjectType(): boolean { + return true; + } + + protected makeRenderer(renderContext: RenderContext, _untypedOptionValues: FixMeOptionsType): JSONSchemaRenderer { + return new JSONSchemaRenderer(this, renderContext); + } +} diff --git a/packages/quicktype-core/src/language/JSONSchema/utils.ts b/packages/quicktype-core/src/language/JSONSchema/utils.ts new file mode 100644 index 000000000..5c3a10e48 --- /dev/null +++ b/packages/quicktype-core/src/language/JSONSchema/utils.ts @@ -0,0 +1,26 @@ +import { funPrefixNamer } from "../../Naming"; +import { + allUpperWordStyle, + combineWords, + firstUpperWordStyle, + legalizeCharacters, + splitIntoWords +} from "../../support/Strings"; + +export const namingFunction = funPrefixNamer("namer", jsonNameStyle); + +const legalizeName = legalizeCharacters(cp => cp >= 32 && cp < 128 && cp !== 0x2f /* slash */); + +function jsonNameStyle(original: string): string { + const words = splitIntoWords(original); + return combineWords( + words, + legalizeName, + firstUpperWordStyle, + firstUpperWordStyle, + allUpperWordStyle, + allUpperWordStyle, + "", + _ => true + ); +} diff --git a/packages/quicktype-core/src/language/Java/DateTimeProvider.ts b/packages/quicktype-core/src/language/Java/DateTimeProvider.ts new file mode 100644 index 000000000..78228db01 --- /dev/null +++ b/packages/quicktype-core/src/language/Java/DateTimeProvider.ts @@ -0,0 +1,261 @@ +import { type Sourcelike } from "../../Source"; + +import { type JavaRenderer } from "./JavaRenderer"; + +export abstract class JavaDateTimeProvider { + public constructor( + protected readonly _renderer: JavaRenderer, + protected readonly _className: string + ) {} + + public abstract keywords: string[]; + + public abstract dateTimeImports: string[]; + + public abstract dateImports: string[]; + + public abstract timeImports: string[]; + + public abstract converterImports: string[]; + + public abstract dateTimeType: string; + + public abstract dateType: string; + + public abstract timeType: string; + + public abstract dateTimeJacksonAnnotations: string[]; + + public abstract dateJacksonAnnotations: string[]; + + public abstract timeJacksonAnnotations: string[]; + + public abstract emitDateTimeConverters(): void; + + public shouldEmitDateTimeConverter = true; + + public shouldEmitTimeConverter = true; + + public shouldEmitDateConverter = true; + + public abstract convertStringToDateTime(variable: Sourcelike): Sourcelike; + public abstract convertStringToTime(variable: Sourcelike): Sourcelike; + public abstract convertStringToDate(variable: Sourcelike): Sourcelike; + + public abstract convertDateTimeToString(variable: Sourcelike): Sourcelike; + public abstract convertTimeToString(variable: Sourcelike): Sourcelike; + public abstract convertDateToString(variable: Sourcelike): Sourcelike; +} + +export class Java8DateTimeProvider extends JavaDateTimeProvider { + public keywords = [ + "LocalDate", + "OffsetDateTime", + "OffsetTime", + "ZoneOffset", + "ZonedDateTime", + "DateTimeFormatter", + "DateTimeFormatterBuilder", + "ChronoField" + ]; + + public dateTimeImports: string[] = ["java.time.OffsetDateTime"]; + + public dateImports: string[] = ["java.time.LocalDate"]; + + public timeImports: string[] = ["java.time.OffsetTime"]; + + public converterImports: string[] = [ + "java.time.LocalDate", + "java.time.OffsetDateTime", + "java.time.OffsetTime", + "java.time.ZoneOffset", + "java.time.ZonedDateTime", + "java.time.format.DateTimeFormatter", + "java.time.format.DateTimeFormatterBuilder", + "java.time.temporal.ChronoField" + ]; + + public dateTimeType = "OffsetDateTime"; + + public dateType = "LocalDate"; + + public timeType = "OffsetTime"; + + public dateTimeJacksonAnnotations: string[] = []; + + public dateJacksonAnnotations: string[] = []; + + public timeJacksonAnnotations: string[] = []; + + public emitDateTimeConverters(): void { + this._renderer.ensureBlankLine(); + this._renderer.emitLine( + "private static final DateTimeFormatter DATE_TIME_FORMATTER = new DateTimeFormatterBuilder()" + ); + this._renderer.indent(() => + this._renderer.indent(() => { + this._renderer.emitLine(".appendOptional(DateTimeFormatter.ISO_DATE_TIME)"); + this._renderer.emitLine(".appendOptional(DateTimeFormatter.ISO_OFFSET_DATE_TIME)"); + this._renderer.emitLine(".appendOptional(DateTimeFormatter.ISO_INSTANT)"); + this._renderer.emitLine('.appendOptional(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SX"))'); + this._renderer.emitLine('.appendOptional(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssX"))'); + this._renderer.emitLine('.appendOptional(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))'); + this._renderer.emitLine(".toFormatter()"); + this._renderer.emitLine(".withZone(ZoneOffset.UTC);"); + }) + ); + this._renderer.ensureBlankLine(); + this._renderer.emitBlock("public static OffsetDateTime parseDateTimeString(String str)", () => { + this._renderer.emitLine( + "return ZonedDateTime.from(Converter.DATE_TIME_FORMATTER.parse(str)).toOffsetDateTime();" + ); + }); + + this._renderer.ensureBlankLine(); + this._renderer.emitLine( + "private static final DateTimeFormatter TIME_FORMATTER = new DateTimeFormatterBuilder()" + ); + this._renderer.indent(() => + this._renderer.indent(() => { + this._renderer.emitLine(".appendOptional(DateTimeFormatter.ISO_TIME)"); + this._renderer.emitLine(".appendOptional(DateTimeFormatter.ISO_OFFSET_TIME)"); + this._renderer.emitLine(".parseDefaulting(ChronoField.YEAR, 2020)"); + this._renderer.emitLine(".parseDefaulting(ChronoField.MONTH_OF_YEAR, 1)"); + this._renderer.emitLine(".parseDefaulting(ChronoField.DAY_OF_MONTH, 1)"); + this._renderer.emitLine(".toFormatter()"); + this._renderer.emitLine(".withZone(ZoneOffset.UTC);"); + }) + ); + this._renderer.ensureBlankLine(); + this._renderer.emitBlock("public static OffsetTime parseTimeString(String str)", () => { + this._renderer.emitLine( + "return ZonedDateTime.from(Converter.TIME_FORMATTER.parse(str)).toOffsetDateTime().toOffsetTime();" + ); + }); + } + + public convertStringToDateTime(variable: Sourcelike): Sourcelike { + return [this._className, ".parseDateTimeString(", variable, ")"]; + } + + public convertStringToTime(variable: Sourcelike): Sourcelike { + return [this._className, ".parseTimeString(", variable, ")"]; + } + + public convertStringToDate(variable: Sourcelike): Sourcelike { + return ["LocalDate.parse(", variable, ")"]; + } + + public convertDateTimeToString(variable: Sourcelike): Sourcelike { + return [variable, ".format(java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME)"]; + } + + public convertTimeToString(variable: Sourcelike): Sourcelike { + return [variable, ".format(java.time.format.DateTimeFormatter.ISO_OFFSET_TIME)"]; + } + + public convertDateToString(variable: Sourcelike): Sourcelike { + return [variable, ".format(java.time.format.DateTimeFormatter.ISO_DATE)"]; + } +} +export class JavaLegacyDateTimeProvider extends JavaDateTimeProvider { + public keywords = ["SimpleDateFormat", "Date"]; + + public dateTimeImports: string[] = ["java.util.Date"]; + + public dateImports: string[] = ["java.util.Date"]; + + public timeImports: string[] = ["java.util.Date"]; + + public converterImports: string[] = ["java.util.Date", "java.text.SimpleDateFormat"]; + + public dateTimeType = "Date"; + + public dateType = "Date"; + + public timeType = "Date"; + + public dateTimeJacksonAnnotations: string[] = [ + '@JsonFormat(pattern = "yyyy-MM-dd\'T\'HH:mm:ssX", timezone = "UTC")' + ]; + + public dateJacksonAnnotations: string[] = ['@JsonFormat(pattern = "yyyy-MM-dd")']; + + public timeJacksonAnnotations: string[] = ['@JsonFormat(pattern = "HH:mm:ssX", timezone = "UTC")']; + + public shouldEmitTimeConverter = false; + + public shouldEmitDateConverter = false; + + public emitDateTimeConverters(): void { + this._renderer.ensureBlankLine(); + this._renderer.emitLine("private static final String[] DATE_TIME_FORMATS = {"); + this._renderer.indent(() => + this._renderer.indent(() => { + this._renderer.emitLine("\"yyyy-MM-dd'T'HH:mm:ss.SX\","); + this._renderer.emitLine("\"yyyy-MM-dd'T'HH:mm:ss.S\","); + this._renderer.emitLine("\"yyyy-MM-dd'T'HH:mm:ssX\","); + this._renderer.emitLine("\"yyyy-MM-dd'T'HH:mm:ss\","); + this._renderer.emitLine('"yyyy-MM-dd HH:mm:ss.SX",'); + this._renderer.emitLine('"yyyy-MM-dd HH:mm:ss.S",'); + this._renderer.emitLine('"yyyy-MM-dd HH:mm:ssX",'); + this._renderer.emitLine('"yyyy-MM-dd HH:mm:ss",'); + this._renderer.emitLine('"HH:mm:ss.SZ",'); + this._renderer.emitLine('"HH:mm:ss.S",'); + this._renderer.emitLine('"HH:mm:ssZ",'); + this._renderer.emitLine('"HH:mm:ss",'); + this._renderer.emitLine('"yyyy-MM-dd",'); + }) + ); + this._renderer.emitLine("};"); + this._renderer.ensureBlankLine(); + this._renderer.emitBlock("public static Date parseAllDateTimeString(String str)", () => { + this._renderer.emitBlock("for (String format : DATE_TIME_FORMATS)", () => { + this._renderer.emitIgnoredTryCatchBlock(() => { + this._renderer.emitLine("return new SimpleDateFormat(format).parse(str);"); + }); + }); + this._renderer.emitLine("return null;"); + }); + + this._renderer.ensureBlankLine(); + this._renderer.emitBlock("public static String serializeDateTime(Date datetime)", () => { + this._renderer.emitLine("return new SimpleDateFormat(\"yyyy-MM-dd'T'hh:mm:ssZ\").format(datetime);"); + }); + + this._renderer.ensureBlankLine(); + this._renderer.emitBlock("public static String serializeDate(Date datetime)", () => { + this._renderer.emitLine('return new SimpleDateFormat("yyyy-MM-dd").format(datetime);'); + }); + + this._renderer.ensureBlankLine(); + this._renderer.emitBlock("public static String serializeTime(Date datetime)", () => { + this._renderer.emitLine('return new SimpleDateFormat("hh:mm:ssZ").format(datetime);'); + }); + } + + public convertStringToDateTime(variable: Sourcelike): Sourcelike { + return [this._className, ".parseAllDateTimeString(", variable, ")"]; + } + + public convertStringToTime(variable: Sourcelike): Sourcelike { + return [this._className, ".parseAllDateTimeString(", variable, ")"]; + } + + public convertStringToDate(variable: Sourcelike): Sourcelike { + return [this._className, ".parseAllDateTimeString(", variable, ")"]; + } + + public convertDateTimeToString(variable: Sourcelike): Sourcelike { + return [this._className, ".serializeDateTime(", variable, ")"]; + } + + public convertTimeToString(variable: Sourcelike): Sourcelike { + return [this._className, ".serializeTime(", variable, ")"]; + } + + public convertDateToString(variable: Sourcelike): Sourcelike { + return [this._className, ".serializeDate(", variable, ")"]; + } +} diff --git a/packages/quicktype-core/src/language/Java/JavaJacksonRenderer.ts b/packages/quicktype-core/src/language/Java/JavaJacksonRenderer.ts new file mode 100644 index 000000000..3329b61cd --- /dev/null +++ b/packages/quicktype-core/src/language/Java/JavaJacksonRenderer.ts @@ -0,0 +1,567 @@ +import { type Name } from "../../Naming"; +import { type RenderContext } from "../../Renderer"; +import { type OptionValues } from "../../RendererOptions"; +import { type Sourcelike } from "../../Source"; +import { assertNever, panic } from "../../support/Support"; +import { type TargetLanguage } from "../../TargetLanguage"; +import { ArrayType, type ClassProperty, ClassType, EnumType, type Type, type TypeKind, UnionType } from "../../Type"; +import { removeNullFromUnion } from "../../TypeUtils"; + +import { JavaRenderer } from "./JavaRenderer"; +import { type javaOptions } from "./language"; +import { stringEscape } from "./utils"; + + +export class JacksonRenderer extends JavaRenderer { + public constructor( + targetLanguage: TargetLanguage, + renderContext: RenderContext, + options: OptionValues + ) { + super(targetLanguage, renderContext, options); + } + + protected readonly _converterKeywords: string[] = [ + "JsonProperty", + "JsonDeserialize", + "JsonDeserializer", + "JsonSerialize", + "JsonSerializer", + "JsonParser", + "JsonProcessingException", + "DeserializationContext", + "SerializerProvider" + ]; + + protected emitClassAttributes(c: ClassType, _className: Name): void { + if (c.getProperties().size === 0) + this.emitLine("@JsonAutoDetect(fieldVisibility=JsonAutoDetect.Visibility.NONE)"); + + super.emitClassAttributes(c, _className); + } + + protected annotationsForAccessor( + _c: ClassType, + _className: Name, + _propertyName: Name, + jsonName: string, + p: ClassProperty, + _isSetter: boolean + ): string[] { + const superAnnotations = super.annotationsForAccessor(_c, _className, _propertyName, jsonName, p, _isSetter); + + const annotations: string[] = ['@JsonProperty("' + stringEscape(jsonName) + '")']; + + switch (p.type.kind) { + case "date-time": + this._dateTimeProvider.dateTimeJacksonAnnotations.forEach(annotation => annotations.push(annotation)); + break; + case "date": + this._dateTimeProvider.dateJacksonAnnotations.forEach(annotation => annotations.push(annotation)); + break; + case "time": + this._dateTimeProvider.timeJacksonAnnotations.forEach(annotation => annotations.push(annotation)); + break; + default: + break; + } + + return [...superAnnotations, ...annotations]; + } + + protected importsForType(t: ClassType | UnionType | EnumType): string[] { + if (t instanceof ClassType) { + const imports = super.importsForType(t); + imports.push("com.fasterxml.jackson.annotation.*"); + return imports; + } + + if (t instanceof UnionType) { + const imports = super.importsForType(t); + imports.push( + "java.io.IOException", + "com.fasterxml.jackson.core.*", + "com.fasterxml.jackson.databind.*", + "com.fasterxml.jackson.databind.annotation.*" + ); + if (this._options.useList) { + imports.push("com.fasterxml.jackson.core.type.*"); + } + + return imports; + } + + if (t instanceof EnumType) { + const imports = super.importsForType(t); + imports.push("com.fasterxml.jackson.annotation.*"); + return imports; + } + + return assertNever(t); + } + + protected emitUnionAttributes(_u: UnionType, unionName: Name): void { + this.emitLine("@JsonDeserialize(using = ", unionName, ".Deserializer.class)"); + this.emitLine("@JsonSerialize(using = ", unionName, ".Serializer.class)"); + } + + protected emitUnionSerializer(u: UnionType, unionName: Name): void { + const stringBasedObjects: TypeKind[] = ["uuid", "time", "date", "date-time"]; + + const tokenCase = (tokenType: string): void => { + this.emitLine("case ", tokenType, ":"); + }; + + const emitNullDeserializer = (): void => { + this.indent(() => { + tokenCase("VALUE_NULL"); + this.indent(() => this.emitLine("break;")); + }); + }; + + const emitDeserializerCodeForStringObjects = ( + fieldName: Sourcelike, + kind: TypeKind, + parseFrom: string + ): void => { + switch (kind) { + case "date": + this.emitLine( + "value.", + fieldName, + " = ", + this._dateTimeProvider.convertStringToDate(parseFrom), + ";" + ); + + break; + case "time": + this.emitLine( + "value.", + fieldName, + " = ", + this._dateTimeProvider.convertStringToTime(parseFrom), + ";" + ); + + break; + case "date-time": + this.emitLine( + "value.", + fieldName, + " = ", + this._dateTimeProvider.convertStringToDateTime(parseFrom), + ";" + ); + break; + case "uuid": + this.emitLine("value.", fieldName, " = UUID.fromString(", parseFrom, ");"); + + break; + default: + return panic("Requested type isnt an object!"); + } + }; + + const emitDeserializeType = (t: Type, variableFieldName = ""): void => { + const { fieldName } = this.unionField(u, t); + const rendered = this.javaTypeWithoutGenerics(true, t); + if (this._options.useList && t instanceof ArrayType) { + this.emitLine( + "value.", + fieldName, + " = jsonParser.readValueAs(new TypeReference<", + rendered, + ">() {});" + ); + } else if (stringBasedObjects.some(stringBasedTypeKind => t.kind === stringBasedTypeKind)) { + emitDeserializerCodeForStringObjects(fieldName, t.kind, variableFieldName); + } else if (t.kind === "string") { + this.emitLine("value.", fieldName, " = ", variableFieldName, ";"); + } else if (t.kind === "enum") { + const { fieldType } = this.unionField(u, t, true); + this.emitLine("value.", fieldName, " = ", fieldType, ".forValue(", variableFieldName, ");"); + } else { + this.emitLine("value.", fieldName, " = jsonParser.readValueAs(", rendered, ".class);"); + } + }; + + const emitDeserializer = (tokenTypes: string[], kind: TypeKind): void => { + const t = u.findMember(kind); + if (t === undefined) return; + + this.indent(() => { + for (const tokenType of tokenTypes) { + tokenCase(tokenType); + } + + this.indent(() => { + emitDeserializeType(t); + this.emitLine("break;"); + }); + }); + }; + + const emitStringDeserializer = (): void => { + const enumType = u.findMember("enum"); + const stringType = u.findMember("string"); + + if ( + stringBasedObjects.every(kind => u.findMember(kind) === undefined) && + stringType === undefined && + enumType === undefined + ) + return; + + this.indent(() => { + tokenCase("VALUE_STRING"); + + this.indent(() => { + const fromVariable = "string"; + this.emitLine("String " + fromVariable + " = jsonParser.readValueAs(String.class);"); + + stringBasedObjects.forEach(kind => { + const type = u.findMember(kind); + if (type !== undefined) { + this.emitIgnoredTryCatchBlock(() => { + emitDeserializeType(type, fromVariable); + }); + } + }); + + if (enumType !== undefined) { + this.emitIgnoredTryCatchBlock(() => { + emitDeserializeType(enumType, fromVariable); + }); + } + + // String should be the last one if exists, because it cannot fail, unlike the parsers. + if (stringType !== undefined) { + emitDeserializeType(stringType, fromVariable); + } + + this.emitLine("break;"); + }); + }); + }; + + const emitNumberDeserializer = (): void => { + const integerType = u.findMember("integer"); + const doubleType = u.findMember("double"); + if (doubleType === undefined && integerType === undefined) return; + + this.indent(() => { + tokenCase("VALUE_NUMBER_INT"); + if (integerType !== undefined) { + this.indent(() => { + emitDeserializeType(integerType); + this.emitLine("break;"); + }); + } + + if (doubleType !== undefined) { + tokenCase("VALUE_NUMBER_FLOAT"); + this.indent(() => { + emitDeserializeType(doubleType); + this.emitLine("break;"); + }); + } + }); + }; + + const customObjectSerializer: TypeKind[] = ["time", "date", "date-time"]; + + const serializerCodeForType = (type: Type, fieldName: Sourcelike): Sourcelike => { + switch (type.kind) { + case "date": + return this._dateTimeProvider.convertDateToString(fieldName); + case "time": + return this._dateTimeProvider.convertTimeToString(fieldName); + case "date-time": + return this._dateTimeProvider.convertDateTimeToString(fieldName); + default: + return panic("Requested type doesn't have custom serializer code!"); + } + }; + + const emitSerializeType = (t: Type): void => { + let { fieldName } = this.unionField(u, t, true); + + this.emitBlock(["if (obj.", fieldName, " != null)"], () => { + if (customObjectSerializer.some(customSerializerType => t.kind === customSerializerType)) { + this.emitLine("jsonGenerator.writeObject(", serializerCodeForType(t, ["obj.", fieldName]), ");"); + } else { + this.emitLine("jsonGenerator.writeObject(obj.", fieldName, ");"); + } + + this.emitLine("return;"); + }); + }; + + const [maybeNull, nonNulls] = removeNullFromUnion(u); + + this.ensureBlankLine(); + this.emitBlock(["static class Deserializer extends JsonDeserializer<", unionName, ">"], () => { + this.emitLine("@Override"); + this.emitBlock( + [ + "public ", + unionName, + " deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException" + ], + () => { + this.emitLine(unionName, " value = new ", unionName, "();"); + this.emitLine("switch (jsonParser.currentToken()) {"); + if (maybeNull !== null) emitNullDeserializer(); + emitNumberDeserializer(); + emitDeserializer(["VALUE_TRUE", "VALUE_FALSE"], "bool"); + emitStringDeserializer(); + emitDeserializer(["START_ARRAY"], "array"); + emitDeserializer(["START_OBJECT"], "class"); + emitDeserializer(["START_OBJECT"], "map"); + this.indent(() => + this.emitLine('default: throw new IOException("Cannot deserialize ', unionName, '");') + ); + this.emitLine("}"); + this.emitLine("return value;"); + } + ); + }); + this.ensureBlankLine(); + this.emitBlock(["static class Serializer extends JsonSerializer<", unionName, ">"], () => { + this.emitLine("@Override"); + this.emitBlock( + [ + "public void serialize(", + unionName, + " obj, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException" + ], + () => { + for (const t of nonNulls) { + emitSerializeType(t); + } + + if (maybeNull !== null) { + this.emitLine("jsonGenerator.writeNull();"); + } else { + this.emitLine('throw new IOException("', unionName, ' must not be null");'); + } + } + ); + }); + } + + protected emitEnumSerializationAttributes(_e: EnumType): void { + this.emitLine("@JsonValue"); + } + + protected emitEnumDeserializationAttributes(_e: EnumType): void { + this.emitLine("@JsonCreator"); + } + + protected emitOffsetDateTimeConverterModule(): void { + this.emitLine("SimpleModule module = new SimpleModule();"); + + if (this._dateTimeProvider.shouldEmitDateTimeConverter) { + this.emitLine( + "module.addDeserializer(", + this._dateTimeProvider.dateTimeType, + ".class, new JsonDeserializer<", + this._dateTimeProvider.dateTimeType, + ">() {" + ); + this.indent(() => { + this.emitLine("@Override"); + this.emitBlock( + [ + "public ", + this._dateTimeProvider.dateTimeType, + " deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) ", + "throws IOException, JsonProcessingException" + ], + () => { + this.emitLine("String value = jsonParser.getText();"); + this.emitLine("return ", this._dateTimeProvider.convertStringToDateTime("value"), ";"); + } + ); + }); + this.emitLine("});"); + } + + if (!this._dateTimeProvider.shouldEmitTimeConverter) { + this.emitLine( + "module.addDeserializer(", + this._dateTimeProvider.timeType, + ".class, new JsonDeserializer<", + this._dateTimeProvider.timeType, + ">() {" + ); + this.indent(() => { + this.emitLine("@Override"); + this.emitBlock( + [ + "public ", + this._dateTimeProvider.timeType, + " deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) ", + "throws IOException, JsonProcessingException" + ], + () => { + this.emitLine("String value = jsonParser.getText();"); + this.emitLine("return ", this._dateTimeProvider.convertStringToTime("value"), ";"); + } + ); + }); + this.emitLine("});"); + } + + if (!this._dateTimeProvider.shouldEmitDateConverter) { + this.emitLine( + "module.addDeserializer(", + this._dateTimeProvider.dateType, + ".class, new JsonDeserializer<", + this._dateTimeProvider.dateType, + ">() {" + ); + this.indent(() => { + this.emitLine("@Override"); + this.emitBlock( + [ + "public ", + this._dateTimeProvider.dateType, + " deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) ", + "throws IOException, JsonProcessingException" + ], + () => { + this.emitLine("String value = jsonParser.getText();"); + this.emitLine("return ", this._dateTimeProvider.convertStringToDate("value"), ";"); + } + ); + }); + this.emitLine("});"); + } + + this.emitLine("mapper.registerModule(module);"); + } + + protected emitConverterClass(): void { + this.startFile(this._converterClassname); + this.emitCommentLines([ + "To use this code, add the following Maven dependency to your project:", + "", + this._options.lombok ? " org.projectlombok : lombok : 1.18.2" : "", + " com.fasterxml.jackson.core : jackson-databind : 2.9.0", + this._options.dateTimeProvider === "java8" + ? " com.fasterxml.jackson.datatype : jackson-datatype-jsr310 : 2.9.0" + : "", + "", + "Import this package:", + "" + ]); + this.emitLine("// import ", this._options.packageName, ".Converter;"); + this.emitMultiline(`// +// Then you can deserialize a JSON string with +//`); + this.forEachTopLevel("none", (t, name) => { + this.emitLine( + "// ", + this.javaType(false, t), + " data = Converter.", + this.decoderName(name), + "(jsonString);" + ); + }); + this.ensureBlankLine(); + const imports = [ + "java.io.IOException", + "com.fasterxml.jackson.databind.*", + "com.fasterxml.jackson.databind.module.SimpleModule", + "com.fasterxml.jackson.core.JsonParser", + "com.fasterxml.jackson.core.JsonProcessingException", + "java.util.*" + ].concat(this._dateTimeProvider.converterImports); + this.emitPackageAndImports(imports); + this.ensureBlankLine(); + this.emitBlock(["public class Converter"], () => { + this.emitLine("// Date-time helpers"); + this._dateTimeProvider.emitDateTimeConverters(); + + this.emitLine("// Serialize/deserialize helpers"); + this.forEachTopLevel("leading-and-interposing", (topLevelType, topLevelName) => { + const topLevelTypeRendered = this.javaType(false, topLevelType); + this.emitBlock( + [ + "public static ", + topLevelTypeRendered, + " ", + this.decoderName(topLevelName), + "(String json) throws IOException" + ], + () => { + this.emitLine("return ", this.readerGetterName(topLevelName), "().readValue(json);"); + } + ); + this.ensureBlankLine(); + this.emitBlock( + [ + "public static String ", + this.encoderName(topLevelName), + "(", + topLevelTypeRendered, + " obj) throws JsonProcessingException" + ], + () => { + this.emitLine("return ", this.writerGetterName(topLevelName), "().writeValueAsString(obj);"); + } + ); + }); + this.forEachTopLevel("leading-and-interposing", (topLevelType, topLevelName) => { + const readerName = this.fieldOrMethodName("reader", topLevelName); + const writerName = this.fieldOrMethodName("writer", topLevelName); + this.emitLine("private static ObjectReader ", readerName, ";"); + this.emitLine("private static ObjectWriter ", writerName, ";"); + this.ensureBlankLine(); + this.emitBlock( + ["private static void ", this.methodName("instantiate", "Mapper", topLevelName), "()"], + () => { + const renderedForClass = this.javaTypeWithoutGenerics(false, topLevelType); + this.emitLine("ObjectMapper mapper = new ObjectMapper();"); + this.emitLine("mapper.findAndRegisterModules();"); + this.emitLine("mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);"); + this.emitLine("mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);"); + this.emitOffsetDateTimeConverterModule(); + this.emitLine(readerName, " = mapper.readerFor(", renderedForClass, ".class);"); + this.emitLine(writerName, " = mapper.writerFor(", renderedForClass, ".class);"); + } + ); + this.ensureBlankLine(); + this.emitBlock(["private static ObjectReader ", this.readerGetterName(topLevelName), "()"], () => { + this.emitLine( + "if (", + readerName, + " == null) ", + this.methodName("instantiate", "Mapper", topLevelName), + "();" + ); + this.emitLine("return ", readerName, ";"); + }); + this.ensureBlankLine(); + this.emitBlock(["private static ObjectWriter ", this.writerGetterName(topLevelName), "()"], () => { + this.emitLine( + "if (", + writerName, + " == null) ", + this.methodName("instantiate", "Mapper", topLevelName), + "();" + ); + this.emitLine("return ", writerName, ";"); + }); + }); + }); + this.finishFile(); + } + + protected emitSourceStructure(): void { + this.emitConverterClass(); + super.emitSourceStructure(); + } +} diff --git a/packages/quicktype-core/src/language/Java/JavaRenderer.ts b/packages/quicktype-core/src/language/Java/JavaRenderer.ts new file mode 100644 index 000000000..1054df770 --- /dev/null +++ b/packages/quicktype-core/src/language/Java/JavaRenderer.ts @@ -0,0 +1,514 @@ +import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../../Annotation"; +import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../../ConvenienceRenderer"; +import { DependencyName, type Name, type Namer, funPrefixNamer } from "../../Naming"; +import { type RenderContext } from "../../Renderer"; +import { type OptionValues } from "../../RendererOptions"; +import { type Sourcelike, maybeAnnotated } from "../../Source"; +import { acronymStyle } from "../../support/Acronyms"; +import { capitalize } from "../../support/Strings"; +import { assert, assertNever, defined } from "../../support/Support"; +import { type TargetLanguage } from "../../TargetLanguage"; +import { ArrayType, type ClassProperty, ClassType, EnumType, MapType, type Type, UnionType } from "../../Type"; +import { directlyReachableSingleNamedType, matchType, nullableFromUnion, removeNullFromUnion } from "../../TypeUtils"; + +import { javaKeywords } from "./constants"; +import { Java8DateTimeProvider, type JavaDateTimeProvider, JavaLegacyDateTimeProvider } from "./DateTimeProvider"; +import { type javaOptions } from "./language"; +import { javaNameStyle, stringEscape } from "./utils"; + +export class JavaRenderer extends ConvenienceRenderer { + private _currentFilename: string | undefined; + + private readonly _gettersAndSettersForPropertyName = new Map(); + + private _haveEmittedLeadingComments = false; + + protected readonly _dateTimeProvider: JavaDateTimeProvider; + + protected readonly _converterClassname: string = "Converter"; + + protected readonly _converterKeywords: string[] = []; + + public constructor( + targetLanguage: TargetLanguage, + renderContext: RenderContext, + protected readonly _options: OptionValues + ) { + super(targetLanguage, renderContext); + + switch (_options.dateTimeProvider) { + default: + case "java8": + this._dateTimeProvider = new Java8DateTimeProvider(this, this._converterClassname); + break; + case "legacy": + this._dateTimeProvider = new JavaLegacyDateTimeProvider(this, this._converterClassname); + break; + } + } + + protected forbiddenNamesForGlobalNamespace(): string[] { + const keywords = [ + ...javaKeywords, + ...this._converterKeywords, + this._converterClassname, + ...this._dateTimeProvider.keywords + ]; + return keywords; + } + + protected forbiddenForObjectProperties(_c: ClassType, _className: Name): ForbiddenWordsInfo { + return { names: [], includeGlobalForbidden: true }; + } + + protected makeNamedTypeNamer(): Namer { + return this.getNameStyling("typeNamingFunction"); + } + + protected namerForObjectProperty(): Namer { + return this.getNameStyling("propertyNamingFunction"); + } + + protected makeUnionMemberNamer(): Namer { + return this.getNameStyling("propertyNamingFunction"); + } + + protected makeEnumCaseNamer(): Namer { + return this.getNameStyling("enumCaseNamingFunction"); + } + + protected unionNeedsName(u: UnionType): boolean { + return nullableFromUnion(u) === null; + } + + protected namedTypeToNameForTopLevel(type: Type): Type | undefined { + // If the top-level type doesn't contain any classes or unions + // we have to define a class just for the `FromJson` method, in + // emitFromJsonForTopLevel. + return directlyReachableSingleNamedType(type); + } + + protected makeNamesForPropertyGetterAndSetter( + _c: ClassType, + _className: Name, + _p: ClassProperty, + _jsonName: string, + name: Name + ): [Name, Name] { + const getterName = new DependencyName( + this.getNameStyling("propertyNamingFunction"), + name.order, + lookup => `get_${lookup(name)}` + ); + const setterName = new DependencyName( + this.getNameStyling("propertyNamingFunction"), + name.order, + lookup => `set_${lookup(name)}` + ); + return [getterName, setterName]; + } + + protected makePropertyDependencyNames( + c: ClassType, + className: Name, + p: ClassProperty, + jsonName: string, + name: Name + ): Name[] { + const getterAndSetterNames = this.makeNamesForPropertyGetterAndSetter(c, className, p, jsonName, name); + this._gettersAndSettersForPropertyName.set(name, getterAndSetterNames); + return getterAndSetterNames; + } + + private getNameStyling(convention: string): Namer { + const styling: { [key: string]: Namer } = { + typeNamingFunction: funPrefixNamer("types", n => + javaNameStyle(true, false, n, acronymStyle(this._options.acronymStyle)) + ), + propertyNamingFunction: funPrefixNamer("properties", n => + javaNameStyle(false, false, n, acronymStyle(this._options.acronymStyle)) + ), + enumCaseNamingFunction: funPrefixNamer("enum-cases", n => + javaNameStyle(true, true, n, acronymStyle(this._options.acronymStyle)) + ) + }; + return styling[convention]; + } + + protected fieldOrMethodName(methodName: string, topLevelName: Name): Sourcelike { + if (this.topLevels.size === 1) { + return methodName; + } + + return [topLevelName, capitalize(methodName)]; + } + + protected methodName(prefix: string, suffix: string, topLevelName: Name): Sourcelike { + if (this.topLevels.size === 1) { + return [prefix, suffix]; + } + + return [prefix, topLevelName, suffix]; + } + + protected decoderName(topLevelName: Name): Sourcelike { + return this.fieldOrMethodName("fromJsonString", topLevelName); + } + + protected encoderName(topLevelName: Name): Sourcelike { + return this.fieldOrMethodName("toJsonString", topLevelName); + } + + protected readerGetterName(topLevelName: Name): Sourcelike { + return this.methodName("get", "ObjectReader", topLevelName); + } + + protected writerGetterName(topLevelName: Name): Sourcelike { + return this.methodName("get", "ObjectWriter", topLevelName); + } + + protected startFile(basename: Sourcelike): void { + assert(this._currentFilename === undefined, "Previous file wasn't finished"); + // FIXME: The filenames should actually be Sourcelikes, too + this._currentFilename = `${this.sourcelikeToString(basename)}.java`; + // FIXME: Why is this necessary? + this.ensureBlankLine(); + if (!this._haveEmittedLeadingComments && this.leadingComments !== undefined) { + this.emitComments(this.leadingComments); + this.ensureBlankLine(); + this._haveEmittedLeadingComments = true; + } + } + + protected finishFile(): void { + super.finishFile(defined(this._currentFilename)); + this._currentFilename = undefined; + } + + protected emitPackageAndImports(imports: string[]): void { + this.emitLine("package ", this._options.packageName, ";"); + this.ensureBlankLine(); + for (const pkg of imports) { + this.emitLine("import ", pkg, ";"); + } + } + + protected emitFileHeader(fileName: Sourcelike, imports: string[]): void { + this.startFile(fileName); + this.emitPackageAndImports(imports); + this.ensureBlankLine(); + } + + public emitDescriptionBlock(lines: Sourcelike[]): void { + this.emitCommentLines(lines, { lineStart: " * ", beforeComment: "/**", afterComment: " */" }); + } + + public emitBlock(line: Sourcelike, f: () => void): void { + this.emitLine(line, " {"); + this.indent(f); + this.emitLine("}"); + } + + public emitTryCatch(main: () => void, handler: () => void, exception = "Exception"): void { + this.emitLine("try {"); + this.indent(main); + this.emitLine("} catch (", exception, " ex) {"); + this.indent(handler); + this.emitLine("}"); + } + + public emitIgnoredTryCatchBlock(f: () => void): void { + this.emitTryCatch(f, () => this.emitLine("// Ignored")); + } + + protected javaType(reference: boolean, t: Type, withIssues = false): Sourcelike { + return matchType( + t, + _anyType => maybeAnnotated(withIssues, anyTypeIssueAnnotation, "Object"), + _nullType => maybeAnnotated(withIssues, nullTypeIssueAnnotation, "Object"), + _boolType => (reference ? "Boolean" : "boolean"), + _integerType => (reference ? "Long" : "long"), + _doubleType => (reference ? "Double" : "double"), + _stringType => "String", + arrayType => { + if (this._options.useList) { + return ["List<", this.javaType(true, arrayType.items, withIssues), ">"]; + } else { + return [this.javaType(false, arrayType.items, withIssues), "[]"]; + } + }, + classType => this.nameForNamedType(classType), + mapType => ["Map"], + enumType => this.nameForNamedType(enumType), + unionType => { + const nullable = nullableFromUnion(unionType); + if (nullable !== null) return this.javaType(true, nullable, withIssues); + return this.nameForNamedType(unionType); + }, + transformedStringType => { + if (transformedStringType.kind === "time") { + return this._dateTimeProvider.timeType; + } + + if (transformedStringType.kind === "date") { + return this._dateTimeProvider.dateType; + } + + if (transformedStringType.kind === "date-time") { + return this._dateTimeProvider.dateTimeType; + } + + if (transformedStringType.kind === "uuid") { + return "UUID"; + } + + return "String"; + } + ); + } + + protected javaImport(t: Type): string[] { + return matchType( + t, + _anyType => [], + _nullType => [], + _boolType => [], + _integerType => [], + _doubleType => [], + _stringType => [], + arrayType => { + if (this._options.useList) { + return [...this.javaImport(arrayType.items), "java.util.List"]; + } else { + return [...this.javaImport(arrayType.items)]; + } + }, + _classType => [], + mapType => [...this.javaImport(mapType.values), "java.util.Map"], + _enumType => [], + unionType => { + const imports: string[] = []; + unionType.members.forEach(type => this.javaImport(type).forEach(imp => imports.push(imp))); + return imports; + }, + transformedStringType => { + if (transformedStringType.kind === "time") { + return this._dateTimeProvider.timeImports; + } + + if (transformedStringType.kind === "date") { + return this._dateTimeProvider.dateImports; + } + + if (transformedStringType.kind === "date-time") { + return this._dateTimeProvider.dateTimeImports; + } + + if (transformedStringType.kind === "uuid") { + return ["java.util.UUID"]; + } + + return []; + } + ); + } + + protected javaTypeWithoutGenerics(reference: boolean, t: Type): Sourcelike { + if (t instanceof ArrayType) { + if (this._options.useList) { + return ["List"]; + } else { + return [this.javaTypeWithoutGenerics(false, t.items), "[]"]; + } + } else if (t instanceof MapType) { + return "Map"; + } else if (t instanceof UnionType) { + const nullable = nullableFromUnion(t); + if (nullable !== null) return this.javaTypeWithoutGenerics(true, nullable); + return this.nameForNamedType(t); + } else { + return this.javaType(reference, t); + } + } + + protected emitClassAttributes(_c: ClassType, _className: Name): void { + if (this._options.lombok) { + this.emitLine("@lombok.Data"); + } + } + + protected annotationsForAccessor( + _c: ClassType, + _className: Name, + _propertyName: Name, + _jsonName: string, + _p: ClassProperty, + _isSetter: boolean + ): string[] { + return []; + } + + protected importsForType(t: ClassType | UnionType | EnumType): string[] { + if (t instanceof ClassType) { + return []; + } + + if (t instanceof UnionType) { + return ["java.io.IOException"]; + } + + if (t instanceof EnumType) { + return ["java.io.IOException"]; + } + + return assertNever(t); + } + + protected importsForClass(c: ClassType): string[] { + const imports: string[] = []; + this.forEachClassProperty(c, "none", (_name, _jsonName, p) => { + this.javaImport(p.type).forEach(imp => imports.push(imp)); + }); + imports.sort(); + return [...new Set(imports)]; + } + + protected importsForUnionMembers(u: UnionType): string[] { + const imports: string[] = []; + const [, nonNulls] = removeNullFromUnion(u); + this.forEachUnionMember(u, nonNulls, "none", null, (_fieldName, t) => { + this.javaImport(t).forEach(imp => imports.push(imp)); + }); + imports.sort(); + return [...new Set(imports)]; + } + + protected emitClassDefinition(c: ClassType, className: Name): void { + let imports = [...this.importsForType(c), ...this.importsForClass(c)]; + + this.emitFileHeader(className, imports); + this.emitDescription(this.descriptionForType(c)); + this.emitClassAttributes(c, className); + this.emitBlock(["public class ", className], () => { + this.forEachClassProperty(c, "none", (name, jsonName, p) => { + if (this._options.lombok && this._options.lombokCopyAnnotations) { + const getter = this.annotationsForAccessor(c, className, name, jsonName, p, false); + const setter = this.annotationsForAccessor(c, className, name, jsonName, p, true); + if (getter.length !== 0) { + this.emitLine("@lombok.Getter(onMethod_ = {" + getter.join(", ") + "})"); + } + + if (setter.length !== 0) { + this.emitLine("@lombok.Setter(onMethod_ = {" + setter.join(", ") + "})"); + } + } + + this.emitLine("private ", this.javaType(false, p.type, true), " ", name, ";"); + }); + if (!this._options.lombok) { + this.forEachClassProperty(c, "leading-and-interposing", (name, jsonName, p) => { + this.emitDescription(this.descriptionForClassProperty(c, jsonName)); + const [getterName, setterName] = defined(this._gettersAndSettersForPropertyName.get(name)); + const rendered = this.javaType(false, p.type); + this.annotationsForAccessor(c, className, name, jsonName, p, false).forEach(annotation => + this.emitLine(annotation) + ); + this.emitLine("public ", rendered, " ", getterName, "() { return ", name, "; }"); + this.annotationsForAccessor(c, className, name, jsonName, p, true).forEach(annotation => + this.emitLine(annotation) + ); + this.emitLine("public void ", setterName, "(", rendered, " value) { this.", name, " = value; }"); + }); + } + }); + this.finishFile(); + } + + protected unionField(u: UnionType, t: Type, withIssues = false): { fieldName: Sourcelike; fieldType: Sourcelike } { + const fieldType = this.javaType(true, t, withIssues); + // FIXME: "Value" should be part of the name. + const fieldName = [this.nameForUnionMember(u, t), "Value"]; + return { fieldType, fieldName }; + } + + protected emitUnionAttributes(_u: UnionType, _unionName: Name): void { + // empty + } + + protected emitUnionSerializer(_u: UnionType, _unionName: Name): void { + // empty + } + + protected emitUnionDefinition(u: UnionType, unionName: Name): void { + const imports = [...this.importsForType(u), ...this.importsForUnionMembers(u)]; + + this.emitFileHeader(unionName, imports); + this.emitDescription(this.descriptionForType(u)); + const [, nonNulls] = removeNullFromUnion(u); + + this.emitUnionAttributes(u, unionName); + this.emitBlock(["public class ", unionName], () => { + for (const t of nonNulls) { + const { fieldType, fieldName } = this.unionField(u, t, true); + this.emitLine("public ", fieldType, " ", fieldName, ";"); + } + + this.emitUnionSerializer(u, unionName); + }); + this.finishFile(); + } + + protected emitEnumSerializationAttributes(_e: EnumType): void { + // Empty + } + + protected emitEnumDeserializationAttributes(_e: EnumType): void { + // Empty + } + + protected emitEnumDefinition(e: EnumType, enumName: Name): void { + this.emitFileHeader(enumName, this.importsForType(e)); + this.emitDescription(this.descriptionForType(e)); + const caseNames: Sourcelike[] = []; + this.forEachEnumCase(e, "none", name => { + if (caseNames.length > 0) caseNames.push(", "); + caseNames.push(name); + }); + caseNames.push(";"); + this.emitBlock(["public enum ", enumName], () => { + this.emitLine(caseNames); + this.ensureBlankLine(); + + this.emitEnumSerializationAttributes(e); + this.emitBlock("public String toValue()", () => { + this.emitLine("switch (this) {"); + this.indent(() => { + this.forEachEnumCase(e, "none", (name, jsonName) => { + this.emitLine("case ", name, ': return "', stringEscape(jsonName), '";'); + }); + }); + this.emitLine("}"); + this.emitLine("return null;"); + }); + this.ensureBlankLine(); + + this.emitEnumDeserializationAttributes(e); + this.emitBlock(["public static ", enumName, " forValue(String value) throws IOException"], () => { + this.forEachEnumCase(e, "none", (name, jsonName) => { + this.emitLine('if (value.equals("', stringEscape(jsonName), '")) return ', name, ";"); + }); + this.emitLine('throw new IOException("Cannot deserialize ', enumName, '");'); + }); + }); + this.finishFile(); + } + + protected emitSourceStructure(): void { + this.forEachNamedType( + "leading-and-interposing", + (c: ClassType, n: Name) => this.emitClassDefinition(c, n), + (e, n) => this.emitEnumDefinition(e, n), + (u, n) => this.emitUnionDefinition(u, n) + ); + } +} diff --git a/packages/quicktype-core/src/language/Java/constants.ts b/packages/quicktype-core/src/language/Java/constants.ts new file mode 100644 index 000000000..d1e2f797e --- /dev/null +++ b/packages/quicktype-core/src/language/Java/constants.ts @@ -0,0 +1,69 @@ +export const javaKeywords = [ + "_", // as of release 9, '_' is a keyword, and may not be used as an identifier + "Object", + "Class", + "System", + "Long", + "Double", + "Boolean", + "String", + "List", + "Map", + "UUID", + "Exception", + "IOException", + "Override", + "abstract", + "continue", + "for", + "new", + "switch", + "assert", + "default", + "goto", + "package", + "synchronized", + "boolean", + "do", + "if", + "private", + "this", + "break", + "double", + "implements", + "protected", + "throw", + "byte", + "else", + "import", + "public", + "throws", + "case", + "enum", + "instanceof", + "return", + "transient", + "catch", + "extends", + "int", + "short", + "try", + "char", + "final", + "interface", + "static", + "void", + "class", + "finally", + "long", + "strictfp", + "volatile", + "const", + "float", + "native", + "super", + "while", + "null", + "false", + "true" +] as const; diff --git a/packages/quicktype-core/src/language/Java/index.ts b/packages/quicktype-core/src/language/Java/index.ts new file mode 100644 index 000000000..ea2ba695e --- /dev/null +++ b/packages/quicktype-core/src/language/Java/index.ts @@ -0,0 +1,3 @@ +export { JavaTargetLanguage, javaOptions } from "./language"; +export { JavaRenderer } from "./JavaRenderer"; +export { JacksonRenderer } from "./JavaJacksonRenderer"; diff --git a/packages/quicktype-core/src/language/Java/language.ts b/packages/quicktype-core/src/language/Java/language.ts new file mode 100644 index 000000000..3d22278b5 --- /dev/null +++ b/packages/quicktype-core/src/language/Java/language.ts @@ -0,0 +1,77 @@ +import { type RenderContext } from "../../Renderer"; +import { BooleanOption, EnumOption, type Option, StringOption, getOptionValues } from "../../RendererOptions"; +import { AcronymStyleOptions, acronymOption } from "../../support/Acronyms"; +import { TargetLanguage } from "../../TargetLanguage"; +import { type PrimitiveStringTypeKind, type TransformedStringTypeKind } from "../../Type"; +import { type StringTypeMapping } from "../../TypeBuilder"; +import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../../types"; + +import { JacksonRenderer } from "./JavaJacksonRenderer"; +import { JavaRenderer } from "./JavaRenderer"; + +export const javaOptions = { + useList: new EnumOption( + "array-type", + "Use T[] or List", + [ + ["array", false], + ["list", true] + ], + "array" + ), + justTypes: new BooleanOption("just-types", "Plain types only", false), + dateTimeProvider: new EnumOption( + "datetime-provider", + "Date time provider type", + [ + ["java8", "java8"], + ["legacy", "legacy"] + ], + "java8" + ), + acronymStyle: acronymOption(AcronymStyleOptions.Pascal), + // FIXME: Do this via a configurable named eventually. + packageName: new StringOption("package", "Generated package name", "NAME", "io.quicktype"), + lombok: new BooleanOption("lombok", "Use lombok", false, "primary"), + lombokCopyAnnotations: new BooleanOption("lombok-copy-annotations", "Copy accessor annotations", true, "secondary") +}; + +export class JavaTargetLanguage extends TargetLanguage { + public constructor() { + super("Java", ["java"], "java"); + } + + protected getOptions(): Array> { + return [ + javaOptions.useList, + javaOptions.justTypes, + javaOptions.dateTimeProvider, + javaOptions.acronymStyle, + javaOptions.packageName, + javaOptions.lombok, + javaOptions.lombokCopyAnnotations + ]; + } + + public get supportsUnionsWithBothNumberTypes(): boolean { + return true; + } + + protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): JavaRenderer { + const options = getOptionValues(javaOptions, untypedOptionValues); + if (options.justTypes) { + return new JavaRenderer(this, renderContext, options); + } + + return new JacksonRenderer(this, renderContext, options); + } + + public get stringTypeMapping(): StringTypeMapping { + const mapping: Map = new Map(); + mapping.set("date", "date"); + mapping.set("time", "time"); + mapping.set("date-time", "date-time"); + mapping.set("uuid", "uuid"); + return mapping; + } +} diff --git a/packages/quicktype-core/src/language/Java/utils.ts b/packages/quicktype-core/src/language/Java/utils.ts new file mode 100644 index 000000000..abc461f47 --- /dev/null +++ b/packages/quicktype-core/src/language/Java/utils.ts @@ -0,0 +1,46 @@ +import { + allLowerWordStyle, + allUpperWordStyle, + combineWords, + escapeNonPrintableMapper, + firstUpperWordStyle, + isAscii, + isDigit, + isLetter, + splitIntoWords, + standardUnicodeHexEscape, + utf16ConcatMap, + utf16LegalizeCharacters +} from "../../support/Strings"; + +export const stringEscape = utf16ConcatMap(escapeNonPrintableMapper(isAscii, standardUnicodeHexEscape)); + +function isStartCharacter(codePoint: number): boolean { + if (codePoint === 0x5f) return true; // underscore + return isAscii(codePoint) && isLetter(codePoint); +} + +function isPartCharacter(codePoint: number): boolean { + return isStartCharacter(codePoint) || (isAscii(codePoint) && isDigit(codePoint)); +} + +const legalizeName = utf16LegalizeCharacters(isPartCharacter); + +export function javaNameStyle( + startWithUpper: boolean, + upperUnderscore: boolean, + original: string, + acronymsStyle: (s: string) => string = allUpperWordStyle +): string { + const words = splitIntoWords(original); + return combineWords( + words, + legalizeName, + upperUnderscore ? allUpperWordStyle : startWithUpper ? firstUpperWordStyle : allLowerWordStyle, + upperUnderscore ? allUpperWordStyle : firstUpperWordStyle, + upperUnderscore || startWithUpper ? allUpperWordStyle : allLowerWordStyle, + acronymsStyle, + upperUnderscore ? "_" : "", + isStartCharacter + ); +} diff --git a/packages/quicktype-core/src/language/JavaScript.ts b/packages/quicktype-core/src/language/JavaScript/JavaScriptRenderer.ts similarity index 84% rename from packages/quicktype-core/src/language/JavaScript.ts rename to packages/quicktype-core/src/language/JavaScript/JavaScriptRenderer.ts index 440502cff..7dfa79b33 100644 --- a/packages/quicktype-core/src/language/JavaScript.ts +++ b/packages/quicktype-core/src/language/JavaScript/JavaScriptRenderer.ts @@ -1,12 +1,12 @@ import { arrayIntercalate } from "collection-utils"; -import { ConvenienceRenderer } from "../ConvenienceRenderer"; -import { type Name, type Namer, funPrefixNamer } from "../Naming"; -import { type RenderContext } from "../Renderer"; -import { BooleanOption, EnumOption, type Option, type OptionValues, getOptionValues } from "../RendererOptions"; -import { type Sourcelike, modifySource } from "../Source"; -import { AcronymStyleOptions, acronymOption, acronymStyle } from "../support/Acronyms"; -import { ConvertersOptions, convertersOption } from "../support/Converters"; +import { ConvenienceRenderer } from "../../ConvenienceRenderer"; +import { type Name, type Namer, funPrefixNamer } from "../../Naming"; +import { type RenderContext } from "../../Renderer"; +import { type OptionValues } from "../../RendererOptions"; +import { type Sourcelike, modifySource } from "../../Source"; +import { acronymStyle } from "../../support/Acronyms"; +import { ConvertersOptions } from "../../support/Converters"; import { allLowerWordStyle, camelCase, @@ -14,46 +14,16 @@ import { combineWords, firstUpperWordStyle, splitIntoWords, - utf16LegalizeCharacters, utf16StringEscape -} from "../support/Strings"; -import { panic } from "../support/Support"; -import { TargetLanguage } from "../TargetLanguage"; -import { - type ClassProperty, - type ClassType, - type ObjectType, - type PrimitiveStringTypeKind, - type TransformedStringTypeKind, - type Type -} from "../Type"; -import { type StringTypeMapping } from "../TypeBuilder"; -import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../types"; -import { directlyReachableSingleNamedType, matchType } from "../TypeUtils"; - -import { isES3IdentifierPart, isES3IdentifierStart } from "./JavaScriptUnicodeMaps"; - -export const javaScriptOptions = { - acronymStyle: acronymOption(AcronymStyleOptions.Pascal), - runtimeTypecheck: new BooleanOption("runtime-typecheck", "Verify JSON.parse results at runtime", true), - runtimeTypecheckIgnoreUnknownProperties: new BooleanOption( - "runtime-typecheck-ignore-unknown-properties", - "Ignore unknown properties when verifying at runtime", - false, - "secondary" - ), - converters: convertersOption(), - rawType: new EnumOption<"json" | "any">( - "raw-type", - "Type of raw input (json by default)", - [ - ["json", "json"], - ["any", "any"] - ], - "json", - "secondary" - ) -}; +} from "../../support/Strings"; +import { panic } from "../../support/Support"; +import { type TargetLanguage } from "../../TargetLanguage"; +import { type ClassProperty, type ClassType, type ObjectType, type Type } from "../../Type"; +import { directlyReachableSingleNamedType, matchType } from "../../TypeUtils"; + +import { type javaScriptOptions } from "./language"; +import { isES3IdentifierStart } from "./unicodeMaps"; +import { legalizeName } from "./utils"; export interface JavaScriptTypeAnnotations { any: string; @@ -65,44 +35,6 @@ export interface JavaScriptTypeAnnotations { stringArray: string; } -export class JavaScriptTargetLanguage extends TargetLanguage { - public constructor(displayName = "JavaScript", names: string[] = ["javascript", "js", "jsx"], extension = "js") { - super(displayName, names, extension); - } - - protected getOptions(): Array> { - return [ - javaScriptOptions.runtimeTypecheck, - javaScriptOptions.runtimeTypecheckIgnoreUnknownProperties, - javaScriptOptions.acronymStyle, - javaScriptOptions.converters, - javaScriptOptions.rawType - ]; - } - - public get stringTypeMapping(): StringTypeMapping { - const mapping: Map = new Map(); - const dateTimeType = "date-time"; - mapping.set("date", dateTimeType); - mapping.set("date-time", dateTimeType); - return mapping; - } - - public get supportsOptionalClassProperties(): boolean { - return true; - } - - public get supportsFullObjectType(): boolean { - return true; - } - - protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): JavaScriptRenderer { - return new JavaScriptRenderer(this, renderContext, getOptionValues(javaScriptOptions, untypedOptionValues)); - } -} - -export const legalizeName = utf16LegalizeCharacters(isES3IdentifierPart); - const identityNamingFunction = funPrefixNamer("properties", s => s); export class JavaScriptRenderer extends ConvenienceRenderer { diff --git a/packages/quicktype-core/src/language/JavaScript/index.ts b/packages/quicktype-core/src/language/JavaScript/index.ts new file mode 100644 index 000000000..12526b3d9 --- /dev/null +++ b/packages/quicktype-core/src/language/JavaScript/index.ts @@ -0,0 +1,2 @@ +export { JavaScriptTargetLanguage, javaScriptOptions } from "./language"; +export { JavaScriptRenderer, type JavaScriptTypeAnnotations } from "./JavaScriptRenderer"; diff --git a/packages/quicktype-core/src/language/JavaScript/language.ts b/packages/quicktype-core/src/language/JavaScript/language.ts new file mode 100644 index 000000000..85d61bbb6 --- /dev/null +++ b/packages/quicktype-core/src/language/JavaScript/language.ts @@ -0,0 +1,68 @@ +import { type RenderContext } from "../../Renderer"; +import { BooleanOption, EnumOption, type Option, getOptionValues } from "../../RendererOptions"; +import { AcronymStyleOptions, acronymOption } from "../../support/Acronyms"; +import { convertersOption } from "../../support/Converters"; +import { TargetLanguage } from "../../TargetLanguage"; +import { type PrimitiveStringTypeKind, type TransformedStringTypeKind } from "../../Type"; +import { type StringTypeMapping } from "../../TypeBuilder"; +import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../../types"; + +import { JavaScriptRenderer } from "./JavaScriptRenderer"; + +export const javaScriptOptions = { + acronymStyle: acronymOption(AcronymStyleOptions.Pascal), + runtimeTypecheck: new BooleanOption("runtime-typecheck", "Verify JSON.parse results at runtime", true), + runtimeTypecheckIgnoreUnknownProperties: new BooleanOption( + "runtime-typecheck-ignore-unknown-properties", + "Ignore unknown properties when verifying at runtime", + false, + "secondary" + ), + converters: convertersOption(), + rawType: new EnumOption<"json" | "any">( + "raw-type", + "Type of raw input (json by default)", + [ + ["json", "json"], + ["any", "any"] + ], + "json", + "secondary" + ) +}; + +export class JavaScriptTargetLanguage extends TargetLanguage { + public constructor(displayName = "JavaScript", names: string[] = ["javascript", "js", "jsx"], extension = "js") { + super(displayName, names, extension); + } + + protected getOptions(): Array> { + return [ + javaScriptOptions.runtimeTypecheck, + javaScriptOptions.runtimeTypecheckIgnoreUnknownProperties, + javaScriptOptions.acronymStyle, + javaScriptOptions.converters, + javaScriptOptions.rawType + ]; + } + + public get stringTypeMapping(): StringTypeMapping { + const mapping: Map = new Map(); + const dateTimeType = "date-time"; + mapping.set("date", dateTimeType); + mapping.set("date-time", dateTimeType); + return mapping; + } + + public get supportsOptionalClassProperties(): boolean { + return true; + } + + public get supportsFullObjectType(): boolean { + return true; + } + + protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): JavaScriptRenderer { + return new JavaScriptRenderer(this, renderContext, getOptionValues(javaScriptOptions, untypedOptionValues)); + } +} diff --git a/packages/quicktype-core/src/language/JavaScriptUnicodeMaps.ts b/packages/quicktype-core/src/language/JavaScript/unicodeMaps.ts similarity index 100% rename from packages/quicktype-core/src/language/JavaScriptUnicodeMaps.ts rename to packages/quicktype-core/src/language/JavaScript/unicodeMaps.ts diff --git a/packages/quicktype-core/src/language/JavaScript/utils.ts b/packages/quicktype-core/src/language/JavaScript/utils.ts new file mode 100644 index 000000000..7acc4c931 --- /dev/null +++ b/packages/quicktype-core/src/language/JavaScript/utils.ts @@ -0,0 +1,5 @@ +import { utf16LegalizeCharacters } from "../../support/Strings"; + +import { isES3IdentifierPart } from "./unicodeMaps"; + +export const legalizeName = utf16LegalizeCharacters(isES3IdentifierPart); diff --git a/packages/quicktype-core/src/language/JavaScriptPropTypes.ts b/packages/quicktype-core/src/language/JavaScriptPropTypes/JavaScriptPropTypesRenderer.ts similarity index 80% rename from packages/quicktype-core/src/language/JavaScriptPropTypes.ts rename to packages/quicktype-core/src/language/JavaScriptPropTypes/JavaScriptPropTypesRenderer.ts index 6754e83e2..7c7fe659e 100644 --- a/packages/quicktype-core/src/language/JavaScriptPropTypes.ts +++ b/packages/quicktype-core/src/language/JavaScriptPropTypes/JavaScriptPropTypesRenderer.ts @@ -1,13 +1,11 @@ -import { panic } from "@glideapps/ts-necessities"; import { arrayIntercalate } from "collection-utils"; -import { ConvenienceRenderer } from "../ConvenienceRenderer"; -import { type Name, type Namer, funPrefixNamer } from "../Naming"; -import { type RenderContext } from "../Renderer"; -import { EnumOption, type Option, type OptionValues, getOptionValues } from "../RendererOptions"; -import { type Sourcelike } from "../Source"; -import { AcronymStyleOptions, acronymOption, acronymStyle } from "../support/Acronyms"; -import { convertersOption } from "../support/Converters"; +import { ConvenienceRenderer } from "../../ConvenienceRenderer"; +import { type Name, type Namer, funPrefixNamer } from "../../Naming"; +import { type RenderContext } from "../../Renderer"; +import { type OptionValues } from "../../RendererOptions"; +import { type Sourcelike } from "../../Source"; +import { acronymStyle } from "../../support/Acronyms"; import { allLowerWordStyle, capitalize, @@ -15,53 +13,22 @@ import { firstUpperWordStyle, splitIntoWords, utf16StringEscape -} from "../support/Strings"; -import { TargetLanguage } from "../TargetLanguage"; -import { type ArrayType, type ClassProperty, type ClassType, type ObjectType, PrimitiveType, type Type } from "../Type"; -import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../types"; -import { directlyReachableSingleNamedType, matchType } from "../TypeUtils"; - -import { legalizeName } from "./JavaScript"; -import { isES3IdentifierStart } from "./JavaScriptUnicodeMaps"; - -export const javaScriptPropTypesOptions = { - acronymStyle: acronymOption(AcronymStyleOptions.Pascal), - converters: convertersOption(), - moduleSystem: new EnumOption( - "module-system", - "Which module system to use", - [ - ["common-js", false], - ["es6", true] - ], - "es6" - ) -}; - -export class JavaScriptPropTypesTargetLanguage extends TargetLanguage { - protected getOptions(): Array> { - return [javaScriptPropTypesOptions.acronymStyle, javaScriptPropTypesOptions.converters]; - } - - public constructor( - displayName = "JavaScript PropTypes", - names: string[] = ["javascript-prop-types"], - extension = "js" - ) { - super(displayName, names, extension); - } - - protected makeRenderer( - renderContext: RenderContext, - untypedOptionValues: FixMeOptionsType - ): JavaScriptPropTypesRenderer { - return new JavaScriptPropTypesRenderer( - this, - renderContext, - getOptionValues(javaScriptPropTypesOptions, untypedOptionValues) - ); - } -} +} from "../../support/Strings"; +import { panic } from "../../support/Support"; +import { type TargetLanguage } from "../../TargetLanguage"; +import { + type ArrayType, + type ClassProperty, + type ClassType, + type ObjectType, + PrimitiveType, + type Type +} from "../../Type"; +import { directlyReachableSingleNamedType, matchType } from "../../TypeUtils"; +import { isES3IdentifierStart } from "../JavaScript/unicodeMaps"; +import { legalizeName } from "../JavaScript/utils"; + +import { type javaScriptPropTypesOptions } from "./language"; const identityNamingFunction = funPrefixNamer("properties", s => s); diff --git a/packages/quicktype-core/src/language/JavaScriptPropTypes/index.ts b/packages/quicktype-core/src/language/JavaScriptPropTypes/index.ts new file mode 100644 index 000000000..273e81878 --- /dev/null +++ b/packages/quicktype-core/src/language/JavaScriptPropTypes/index.ts @@ -0,0 +1,2 @@ +export { JavaScriptPropTypesTargetLanguage, javaScriptPropTypesOptions } from "./language"; +export { JavaScriptPropTypesRenderer } from "./JavaScriptPropTypesRenderer"; diff --git a/packages/quicktype-core/src/language/JavaScriptPropTypes/language.ts b/packages/quicktype-core/src/language/JavaScriptPropTypes/language.ts new file mode 100644 index 000000000..8ba620927 --- /dev/null +++ b/packages/quicktype-core/src/language/JavaScriptPropTypes/language.ts @@ -0,0 +1,47 @@ +import { type RenderContext } from "../../Renderer"; +import { EnumOption, type Option, getOptionValues } from "../../RendererOptions"; +import { AcronymStyleOptions, acronymOption } from "../../support/Acronyms"; +import { convertersOption } from "../../support/Converters"; +import { TargetLanguage } from "../../TargetLanguage"; +import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../../types"; + +import { JavaScriptPropTypesRenderer } from "./JavaScriptPropTypesRenderer"; + +export const javaScriptPropTypesOptions = { + acronymStyle: acronymOption(AcronymStyleOptions.Pascal), + converters: convertersOption(), + moduleSystem: new EnumOption( + "module-system", + "Which module system to use", + [ + ["common-js", false], + ["es6", true] + ], + "es6" + ) +}; + +export class JavaScriptPropTypesTargetLanguage extends TargetLanguage { + protected getOptions(): Array> { + return [javaScriptPropTypesOptions.acronymStyle, javaScriptPropTypesOptions.converters]; + } + + public constructor( + displayName = "JavaScript PropTypes", + names: string[] = ["javascript-prop-types"], + extension = "js" + ) { + super(displayName, names, extension); + } + + protected makeRenderer( + renderContext: RenderContext, + untypedOptionValues: FixMeOptionsType + ): JavaScriptPropTypesRenderer { + return new JavaScriptPropTypesRenderer( + this, + renderContext, + getOptionValues(javaScriptPropTypesOptions, untypedOptionValues) + ); + } +} diff --git a/packages/quicktype-core/src/language/Kotlin.ts b/packages/quicktype-core/src/language/Kotlin.ts deleted file mode 100644 index d2f759506..000000000 --- a/packages/quicktype-core/src/language/Kotlin.ts +++ /dev/null @@ -1,1129 +0,0 @@ -import { arrayIntercalate, iterableSome } from "collection-utils"; - -import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../Annotation"; -import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../ConvenienceRenderer"; -import { type Name, type Namer, funPrefixNamer } from "../Naming"; -import { type RenderContext } from "../Renderer"; -import { EnumOption, type Option, type OptionValues, StringOption, getOptionValues } from "../RendererOptions"; -import { type Sourcelike, maybeAnnotated, modifySource } from "../Source"; -import { AcronymStyleOptions, acronymOption, acronymStyle } from "../support/Acronyms"; -import { - allLowerWordStyle, - allUpperWordStyle, - camelCase, - combineWords, - escapeNonPrintableMapper, - firstUpperWordStyle, - intToHex, - isDigit, - isLetterOrUnderscore, - isNumeric, - isPrintable, - legalizeCharacters, - splitIntoWords, - utf32ConcatMap -} from "../support/Strings"; -import { assertNever, mustNotHappen } from "../support/Support"; -import { TargetLanguage } from "../TargetLanguage"; -import { - ArrayType, - type ClassProperty, - ClassType, - type EnumType, - MapType, - type ObjectType, - type PrimitiveType, - type Type, - UnionType -} from "../Type"; -import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../types"; -import { matchType, nullableFromUnion, removeNullFromUnion } from "../TypeUtils"; - -export enum Framework { - None = "None", - Jackson = "Jackson", - Klaxon = "Klaxon", - KotlinX = "KotlinX" -} - -export const kotlinOptions = { - framework: new EnumOption( - "framework", - "Serialization framework", - [ - ["just-types", Framework.None], - ["jackson", Framework.Jackson], - ["klaxon", Framework.Klaxon], - ["kotlinx", Framework.KotlinX] - ], - "klaxon" - ), - acronymStyle: acronymOption(AcronymStyleOptions.Pascal), - packageName: new StringOption("package", "Package", "PACKAGE", "quicktype") -}; - -export class KotlinTargetLanguage extends TargetLanguage { - public constructor() { - super("Kotlin", ["kotlin"], "kt"); - } - - protected getOptions(): Array> { - return [kotlinOptions.framework, kotlinOptions.acronymStyle, kotlinOptions.packageName]; - } - - public get supportsOptionalClassProperties(): boolean { - return true; - } - - public get supportsUnionsWithBothNumberTypes(): boolean { - return true; - } - - protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): ConvenienceRenderer { - const options = getOptionValues(kotlinOptions, untypedOptionValues); - - switch (options.framework) { - case Framework.None: - return new KotlinRenderer(this, renderContext, options); - case Framework.Jackson: - return new KotlinJacksonRenderer(this, renderContext, options); - case Framework.Klaxon: - return new KotlinKlaxonRenderer(this, renderContext, options); - case Framework.KotlinX: - return new KotlinXRenderer(this, renderContext, options); - default: - return assertNever(options.framework); - } - } -} - -const keywords = [ - "package", - "as", - "typealias", - "class", - "this", - "super", - "val", - "var", - "fun", - "for", - "null", - "true", - "false", - "is", - "in", - "throw", - "return", - "break", - "continue", - "object", - "if", - "try", - "else", - "while", - "do", - "when", - "interface", - "typeof", - "klaxon", - "toJson", - "Any", - "Boolean", - "Double", - "Float", - "Long", - "Int", - "Short", - "System", - "Byte", - "String", - "Array", - "List", - "Map", - "Enum", - "Class", - "JsonObject", - "JsonValue", - "Converter", - "Klaxon" -]; - -function isPartCharacter(codePoint: number): boolean { - return isLetterOrUnderscore(codePoint) || isNumeric(codePoint); -} - -function isStartCharacter(codePoint: number): boolean { - return isPartCharacter(codePoint) && !isDigit(codePoint); -} - -const legalizeName = legalizeCharacters(isPartCharacter); - -function kotlinNameStyle( - isUpper: boolean, - original: string, - acronymsStyle: (s: string) => string = allUpperWordStyle -): string { - const words = splitIntoWords(original); - return combineWords( - words, - legalizeName, - isUpper ? firstUpperWordStyle : allLowerWordStyle, - firstUpperWordStyle, - isUpper ? allUpperWordStyle : allLowerWordStyle, - acronymsStyle, - "", - isStartCharacter - ); -} - -function unicodeEscape(codePoint: number): string { - return "\\u" + intToHex(codePoint, 4); -} - -// eslint-disable-next-line @typescript-eslint/naming-convention -const _stringEscape = utf32ConcatMap(escapeNonPrintableMapper(isPrintable, unicodeEscape)); - -function stringEscape(s: string): string { - // "$this" is a template string in Kotlin so we have to escape $ - return _stringEscape(s).replace(/\$/g, "\\$"); -} - -export class KotlinRenderer extends ConvenienceRenderer { - public constructor( - targetLanguage: TargetLanguage, - renderContext: RenderContext, - protected readonly _kotlinOptions: OptionValues - ) { - super(targetLanguage, renderContext); - } - - protected forbiddenNamesForGlobalNamespace(): string[] { - return keywords; - } - - protected forbiddenForObjectProperties(_o: ObjectType, _classNamed: Name): ForbiddenWordsInfo { - return { names: [], includeGlobalForbidden: true }; - } - - protected forbiddenForEnumCases(_e: EnumType, _enumName: Name): ForbiddenWordsInfo { - return { names: [], includeGlobalForbidden: true }; - } - - protected forbiddenForUnionMembers(_u: UnionType, _unionName: Name): ForbiddenWordsInfo { - return { names: [], includeGlobalForbidden: false }; - } - - protected topLevelNameStyle(rawName: string): string { - return kotlinNameStyle(true, rawName); - } - - protected makeNamedTypeNamer(): Namer { - return funPrefixNamer("upper", s => kotlinNameStyle(true, s, acronymStyle(this._kotlinOptions.acronymStyle))); - } - - protected namerForObjectProperty(): Namer { - return funPrefixNamer("lower", s => kotlinNameStyle(false, s, acronymStyle(this._kotlinOptions.acronymStyle))); - } - - protected makeUnionMemberNamer(): Namer { - return funPrefixNamer("upper", s => kotlinNameStyle(true, s) + "Value"); - } - - protected makeEnumCaseNamer(): Namer { - return funPrefixNamer("upper", s => kotlinNameStyle(true, s, acronymStyle(this._kotlinOptions.acronymStyle))); - } - - protected emitDescriptionBlock(lines: Sourcelike[]): void { - this.emitCommentLines(lines, { lineStart: " * ", beforeComment: "/**", afterComment: " */" }); - } - - protected emitBlock(line: Sourcelike, f: () => void, delimiter: "curly" | "paren" | "lambda" = "curly"): void { - const [open, close] = delimiter === "curly" ? ["{", "}"] : delimiter === "paren" ? ["(", ")"] : ["{", "})"]; - this.emitLine(line, " ", open); - this.indent(f); - this.emitLine(close); - } - - protected anySourceType(optional: string): Sourcelike { - return ["Any", optional]; - } - - // (asarazan): I've broken out the following two functions - // because some renderers, such as kotlinx, can cope with `any`, while some get mad. - protected arrayType(arrayType: ArrayType, withIssues = false, _noOptional = false): Sourcelike { - return ["List<", this.kotlinType(arrayType.items, withIssues), ">"]; - } - - protected mapType(mapType: MapType, withIssues = false, _noOptional = false): Sourcelike { - return ["Map"]; - } - - protected kotlinType(t: Type, withIssues = false, noOptional = false): Sourcelike { - const optional = noOptional ? "" : "?"; - return matchType( - t, - _anyType => { - return maybeAnnotated(withIssues, anyTypeIssueAnnotation, this.anySourceType(optional)); - }, - _nullType => { - return maybeAnnotated(withIssues, nullTypeIssueAnnotation, this.anySourceType(optional)); - }, - _boolType => "Boolean", - _integerType => "Long", - _doubleType => "Double", - _stringType => "String", - arrayType => this.arrayType(arrayType, withIssues), - classType => this.nameForNamedType(classType), - mapType => this.mapType(mapType, withIssues), - enumType => this.nameForNamedType(enumType), - unionType => { - const nullable = nullableFromUnion(unionType); - if (nullable !== null) return [this.kotlinType(nullable, withIssues), optional]; - return this.nameForNamedType(unionType); - } - ); - } - - protected emitUsageHeader(): void { - // To be overridden - } - - protected emitHeader(): void { - if (this.leadingComments !== undefined) { - this.emitComments(this.leadingComments); - } else { - this.emitUsageHeader(); - } - - this.ensureBlankLine(); - this.emitLine("package ", this._kotlinOptions.packageName); - this.ensureBlankLine(); - } - - protected emitTopLevelPrimitive(t: PrimitiveType, name: Name): void { - const elementType = this.kotlinType(t); - this.emitLine(["typealias ", name, " = ", elementType, ""]); - } - - protected emitTopLevelArray(t: ArrayType, name: Name): void { - const elementType = this.kotlinType(t.items); - this.emitLine(["typealias ", name, " = ArrayList<", elementType, ">"]); - } - - protected emitTopLevelMap(t: MapType, name: Name): void { - const elementType = this.kotlinType(t.values); - this.emitLine(["typealias ", name, " = HashMap"]); - } - - protected emitEmptyClassDefinition(c: ClassType, className: Name): void { - this.emitDescription(this.descriptionForType(c)); - this.emitClassAnnotations(c, className); - this.emitLine("class ", className, "()"); - } - - protected emitClassDefinition(c: ClassType, className: Name): void { - if (c.getProperties().size === 0) { - this.emitEmptyClassDefinition(c, className); - return; - } - - const kotlinType = (p: ClassProperty): Sourcelike => { - if (p.isOptional) { - return [this.kotlinType(p.type, true, true), "?"]; - } else { - return this.kotlinType(p.type, true); - } - }; - - this.emitDescription(this.descriptionForType(c)); - this.emitClassAnnotations(c, className); - this.emitLine("data class ", className, " ("); - this.indent(() => { - let count = c.getProperties().size; - let first = true; - this.forEachClassProperty(c, "none", (name, jsonName, p) => { - const nullable = p.type.kind === "union" && nullableFromUnion(p.type as UnionType) !== null; - const nullableOrOptional = p.isOptional || p.type.kind === "null" || nullable; - const last = --count === 0; - let meta: Array<() => void> = []; - - const description = this.descriptionForClassProperty(c, jsonName); - if (description !== undefined) { - meta.push(() => this.emitDescription(description)); - } - - this.renameAttribute(name, jsonName, !nullableOrOptional, meta); - - if (meta.length > 0 && !first) { - this.ensureBlankLine(); - } - - for (const emit of meta) { - emit(); - } - - this.emitLine("val ", name, ": ", kotlinType(p), nullableOrOptional ? " = null" : "", last ? "" : ","); - - if (meta.length > 0 && !last) { - this.ensureBlankLine(); - } - - first = false; - }); - }); - - this.emitClassDefinitionMethods(c, className); - } - - protected emitClassDefinitionMethods(_c: ClassType, _className: Name): void { - this.emitLine(")"); - } - - protected emitClassAnnotations(_c: Type, _className: Name): void { - // to be overridden - } - - protected renameAttribute(_name: Name, _jsonName: string, _required: boolean, _meta: Array<() => void>): void { - // to be overridden - } - - protected emitEnumDefinition(e: EnumType, enumName: Name): void { - this.emitDescription(this.descriptionForType(e)); - - this.emitBlock(["enum class ", enumName], () => { - let count = e.cases.size; - this.forEachEnumCase(e, "none", name => { - this.emitLine(name, --count === 0 ? "" : ","); - }); - }); - } - - protected emitUnionDefinition(u: UnionType, unionName: Name): void { - function sortBy(t: Type): string { - const kind = t.kind; - if (kind === "class") return kind; - return "_" + kind; - } - - this.emitDescription(this.descriptionForType(u)); - - const [maybeNull, nonNulls] = removeNullFromUnion(u, sortBy); - this.emitClassAnnotations(u, unionName); - this.emitBlock(["sealed class ", unionName], () => { - { - let table: Sourcelike[][] = []; - this.forEachUnionMember(u, nonNulls, "none", null, (name, t) => { - table.push([ - ["class ", name, "(val value: ", this.kotlinType(t), ")"], - [" : ", unionName, "()"] - ]); - }); - if (maybeNull !== null) { - table.push([ - ["class ", this.nameForUnionMember(u, maybeNull), "()"], - [" : ", unionName, "()"] - ]); - } - - this.emitTable(table); - } - - this.emitUnionDefinitionMethods(u, nonNulls, maybeNull, unionName); - }); - } - - protected emitUnionDefinitionMethods( - _u: UnionType, - _nonNulls: ReadonlySet, - _maybeNull: PrimitiveType | null, - _unionName: Name - ): void { - // to be overridden - } - - protected emitSourceStructure(): void { - this.emitHeader(); - - // Top-level arrays, maps - this.forEachTopLevel("leading", (t, name) => { - if (t instanceof ArrayType) { - this.emitTopLevelArray(t, name); - } else if (t instanceof MapType) { - this.emitTopLevelMap(t, name); - } else if (t.isPrimitive()) { - this.emitTopLevelPrimitive(t, name); - } - }); - - this.forEachNamedType( - "leading-and-interposing", - (c: ClassType, n: Name) => this.emitClassDefinition(c, n), - (e, n) => this.emitEnumDefinition(e, n), - (u, n) => this.emitUnionDefinition(u, n) - ); - } -} - -export class KotlinKlaxonRenderer extends KotlinRenderer { - public constructor( - targetLanguage: TargetLanguage, - renderContext: RenderContext, - _kotlinOptions: OptionValues - ) { - super(targetLanguage, renderContext, _kotlinOptions); - } - - private unionMemberFromJsonValue(t: Type, e: Sourcelike): Sourcelike { - return matchType( - t, - _anyType => [e, ".inside"], - _nullType => "null", - _boolType => [e, ".boolean"], - _integerType => ["(", e, ".int?.toLong() ?: ", e, ".longValue)"], - _doubleType => [e, ".double"], - _stringType => [e, ".string"], - arrayType => [e, ".array?.let { klaxon.parseFromJsonArray<", this.kotlinType(arrayType.items), ">(it) }"], - _classType => [e, ".obj?.let { klaxon.parseFromJsonObject<", this.kotlinType(t), ">(it) }"], - _mapType => [e, ".obj?.let { klaxon.parseFromJsonObject<", this.kotlinType(t), ">(it) }"], - enumType => [e, ".string?.let { ", this.kotlinType(enumType), ".fromValue(it) }"], - _unionType => mustNotHappen() - ); - } - - private unionMemberJsonValueGuard(t: Type, _e: Sourcelike): Sourcelike { - return matchType( - t, - _anyType => "is Any", - _nullType => "null", - _boolType => "is Boolean", - _integerType => "is Int, is Long", - _doubleType => "is Double", - _stringType => "is String", - _arrayType => "is JsonArray<*>", - // These could be stricter, but for now we don't allow maps - // and objects in the same union - _classType => "is JsonObject", - _mapType => "is JsonObject", - // This could be stricter, but for now we don't allow strings - // and enums in the same union - _enumType => "is String", - _unionType => mustNotHappen() - ); - } - - protected emitUsageHeader(): void { - this.emitLine("// To parse the JSON, install Klaxon and do:"); - this.emitLine("//"); - this.forEachTopLevel("none", (_, name) => { - this.emitLine("// val ", modifySource(camelCase, name), " = ", name, ".fromJson(jsonString)"); - }); - } - - protected emitHeader(): void { - super.emitHeader(); - - this.emitLine("import com.beust.klaxon.*"); - - const hasUnions = iterableSome( - this.typeGraph.allNamedTypes(), - t => t instanceof UnionType && nullableFromUnion(t) === null - ); - const hasEmptyObjects = iterableSome( - this.typeGraph.allNamedTypes(), - c => c instanceof ClassType && c.getProperties().size === 0 - ); - if (hasUnions || this.haveEnums || hasEmptyObjects) { - this.emitGenericConverter(); - } - - let converters: Sourcelike[][] = []; - if (hasEmptyObjects) { - converters.push([[".convert(JsonObject::class,"], [" { it.obj!! },"], [" { it.toJsonString() })"]]); - } - - this.forEachEnum("none", (_, name) => { - converters.push([ - [".convert(", name, "::class,"], - [" { ", name, ".fromValue(it.string!!) },"], - [' { "\\"${it.value}\\"" })'] - ]); - }); - this.forEachUnion("none", (_, name) => { - converters.push([ - [".convert(", name, "::class,"], - [" { ", name, ".fromJson(it) },"], - [" { it.toJson() }, true)"] - ]); - }); - - this.ensureBlankLine(); - this.emitLine("private val klaxon = Klaxon()"); - if (converters.length > 0) { - this.indent(() => this.emitTable(converters)); - } - } - - protected emitTopLevelArray(t: ArrayType, name: Name): void { - const elementType = this.kotlinType(t.items); - this.emitBlock( - ["class ", name, "(elements: Collection<", elementType, ">) : ArrayList<", elementType, ">(elements)"], - () => { - this.emitLine("public fun toJson() = klaxon.toJsonString(this)"); - this.ensureBlankLine(); - this.emitBlock("companion object", () => { - this.emitLine( - "public fun fromJson(json: String) = ", - name, - "(klaxon.parseArray<", - elementType, - ">(json)!!)" - ); - }); - } - ); - } - - protected emitTopLevelMap(t: MapType, name: Name): void { - const elementType = this.kotlinType(t.values); - this.emitBlock( - [ - "class ", - name, - "(elements: Map) : HashMap(elements)" - ], - () => { - this.emitLine("public fun toJson() = klaxon.toJsonString(this)"); - this.ensureBlankLine(); - this.emitBlock("companion object", () => { - this.emitBlock( - ["public fun fromJson(json: String) = ", name], - () => { - this.emitLine( - "klaxon.parseJsonObject(java.io.StringReader(json)) as Map" - ); - }, - "paren" - ); - }); - } - ); - } - - private klaxonRenameAttribute(propName: Name, jsonName: string, ignore = false): Sourcelike | undefined { - const escapedName = stringEscape(jsonName); - const namesDiffer = this.sourcelikeToString(propName) !== escapedName; - const properties: Sourcelike[] = []; - if (namesDiffer) { - properties.push(['name = "', escapedName, '"']); - } - - if (ignore) { - properties.push("ignored = true"); - } - - return properties.length === 0 ? undefined : ["@Json(", arrayIntercalate(", ", properties), ")"]; - } - - protected emitEmptyClassDefinition(c: ClassType, className: Name): void { - this.emitDescription(this.descriptionForType(c)); - - this.emitLine("typealias ", className, " = JsonObject"); - } - - protected emitClassDefinitionMethods(c: ClassType, className: Name): void { - const isTopLevel = iterableSome(this.topLevels, ([_, top]) => top === c); - if (isTopLevel) { - this.emitBlock(")", () => { - this.emitLine("public fun toJson() = klaxon.toJsonString(this)"); - this.ensureBlankLine(); - this.emitBlock("companion object", () => { - this.emitLine("public fun fromJson(json: String) = klaxon.parse<", className, ">(json)"); - }); - }); - } else { - this.emitLine(")"); - } - } - - protected renameAttribute(name: Name, jsonName: string, _required: boolean, meta: Array<() => void>): void { - const rename = this.klaxonRenameAttribute(name, jsonName); - if (rename !== undefined) { - meta.push(() => this.emitLine(rename)); - } - } - - protected emitEnumDefinition(e: EnumType, enumName: Name): void { - this.emitDescription(this.descriptionForType(e)); - - this.emitBlock(["enum class ", enumName, "(val value: String)"], () => { - let count = e.cases.size; - this.forEachEnumCase(e, "none", (name, json) => { - this.emitLine(name, `("${stringEscape(json)}")`, --count === 0 ? ";" : ","); - }); - this.ensureBlankLine(); - this.emitBlock("companion object", () => { - this.emitBlock(["public fun fromValue(value: String): ", enumName, " = when (value)"], () => { - let table: Sourcelike[][] = []; - this.forEachEnumCase(e, "none", (name, json) => { - table.push([[`"${stringEscape(json)}"`], [" -> ", name]]); - }); - table.push([["else"], [" -> throw IllegalArgumentException()"]]); - this.emitTable(table); - }); - }); - }); - } - - private emitGenericConverter(): void { - this.ensureBlankLine(); - this.emitLine( - "private fun Klaxon.convert(k: kotlin.reflect.KClass<*>, fromJson: (JsonValue) -> T, toJson: (T) -> String, isUnion: Boolean = false) =" - ); - this.indent(() => { - this.emitLine("this.converter(object: Converter {"); - this.indent(() => { - this.emitLine('@Suppress("UNCHECKED_CAST")'); - this.emitTable([ - ["override fun toJson(value: Any)", " = toJson(value as T)"], - ["override fun fromJson(jv: JsonValue)", " = fromJson(jv) as Any"], - [ - "override fun canConvert(cls: Class<*>)", - " = cls == k.java || (isUnion && cls.superclass == k.java)" - ] - ]); - }); - this.emitLine("})"); - }); - } - - protected emitUnionDefinitionMethods( - u: UnionType, - nonNulls: ReadonlySet, - maybeNull: PrimitiveType | null, - unionName: Name - ): void { - this.ensureBlankLine(); - this.emitLine("public fun toJson(): String = klaxon.toJsonString(when (this) {"); - this.indent(() => { - let toJsonTable: Sourcelike[][] = []; - this.forEachUnionMember(u, nonNulls, "none", null, name => { - toJsonTable.push([["is ", name], [" -> this.value"]]); - }); - if (maybeNull !== null) { - const name = this.nameForUnionMember(u, maybeNull); - toJsonTable.push([["is ", name], [' -> "null"']]); - } - - this.emitTable(toJsonTable); - }); - this.emitLine("})"); - this.ensureBlankLine(); - this.emitBlock("companion object", () => { - this.emitLine("public fun fromJson(jv: JsonValue): ", unionName, " = when (jv.inside) {"); - this.indent(() => { - let table: Sourcelike[][] = []; - this.forEachUnionMember(u, nonNulls, "none", null, (name, t) => { - table.push([ - [this.unionMemberJsonValueGuard(t, "jv.inside")], - [" -> ", name, "(", this.unionMemberFromJsonValue(t, "jv"), "!!)"] - ]); - }); - if (maybeNull !== null) { - const name = this.nameForUnionMember(u, maybeNull); - table.push([[this.unionMemberJsonValueGuard(maybeNull, "jv.inside")], [" -> ", name, "()"]]); - } - - table.push([["else"], [" -> throw IllegalArgumentException()"]]); - this.emitTable(table); - }); - this.emitLine("}"); - }); - } -} - -export class KotlinJacksonRenderer extends KotlinRenderer { - public constructor( - targetLanguage: TargetLanguage, - renderContext: RenderContext, - _kotlinOptions: OptionValues - ) { - super(targetLanguage, renderContext, _kotlinOptions); - } - - private unionMemberJsonValueGuard(t: Type, _e: Sourcelike): Sourcelike { - return matchType( - t, - _anyType => "is Any", - _nullType => "null", - _boolType => "is BooleanNode", - _integerType => "is IntNode, is LongNode", - _doubleType => "is DoubleNode", - _stringType => "is TextNode", - _arrayType => "is ArrayNode", - // These could be stricter, but for now we don't allow maps - // and objects in the same union - _classType => "is ObjectNode", - _mapType => "is ObjectNode", - // This could be stricter, but for now we don't allow strings - // and enums in the same union - _enumType => "is TextNode", - _unionType => mustNotHappen() - ); - } - - protected emitUsageHeader(): void { - this.emitLine("// To parse the JSON, install jackson-module-kotlin and do:"); - this.emitLine("//"); - this.forEachTopLevel("none", (_, name) => { - this.emitLine("// val ", modifySource(camelCase, name), " = ", name, ".fromJson(jsonString)"); - }); - } - - protected emitHeader(): void { - super.emitHeader(); - - this.emitMultiline(`import com.fasterxml.jackson.annotation.* -import com.fasterxml.jackson.core.* -import com.fasterxml.jackson.databind.* -import com.fasterxml.jackson.databind.deser.std.StdDeserializer -import com.fasterxml.jackson.databind.module.SimpleModule -import com.fasterxml.jackson.databind.node.* -import com.fasterxml.jackson.databind.ser.std.StdSerializer -import com.fasterxml.jackson.module.kotlin.*`); - - const hasUnions = iterableSome( - this.typeGraph.allNamedTypes(), - t => t instanceof UnionType && nullableFromUnion(t) === null - ); - const hasEmptyObjects = iterableSome( - this.typeGraph.allNamedTypes(), - c => c instanceof ClassType && c.getProperties().size === 0 - ); - if (hasUnions || this.haveEnums || hasEmptyObjects) { - this.emitGenericConverter(); - } - - let converters: Sourcelike[][] = []; - // if (hasEmptyObjects) { - // converters.push([["convert(JsonNode::class,"], [" { it },"], [" { writeValueAsString(it) })"]]); - // } - this.forEachEnum("none", (_, name) => { - converters.push([ - ["convert(", name, "::class,"], - [" { ", name, ".fromValue(it.asText()) },"], - [' { "\\"${it.value}\\"" })'] - ]); - }); - this.forEachUnion("none", (_, name) => { - converters.push([ - ["convert(", name, "::class,"], - [" { ", name, ".fromJson(it) },"], - [" { it.toJson() }, true)"] - ]); - }); - - this.ensureBlankLine(); - this.emitLine("val mapper = jacksonObjectMapper().apply {"); - this.indent(() => { - this.emitLine("propertyNamingStrategy = PropertyNamingStrategy.LOWER_CAMEL_CASE"); - this.emitLine("setSerializationInclusion(JsonInclude.Include.NON_NULL)"); - }); - - if (converters.length > 0) { - this.indent(() => this.emitTable(converters)); - } - - this.emitLine("}"); - } - - protected emitTopLevelArray(t: ArrayType, name: Name): void { - const elementType = this.kotlinType(t.items); - this.emitBlock( - ["class ", name, "(elements: Collection<", elementType, ">) : ArrayList<", elementType, ">(elements)"], - () => { - this.emitLine("fun toJson() = mapper.writeValueAsString(this)"); - this.ensureBlankLine(); - this.emitBlock("companion object", () => { - this.emitLine("fun fromJson(json: String) = mapper.readValue<", name, ">(json)"); - }); - } - ); - } - - protected emitTopLevelMap(t: MapType, name: Name): void { - const elementType = this.kotlinType(t.values); - this.emitBlock( - [ - "class ", - name, - "(elements: Map) : HashMap(elements)" - ], - () => { - this.emitLine("fun toJson() = mapper.writeValueAsString(this)"); - this.ensureBlankLine(); - this.emitBlock("companion object", () => { - this.emitLine("fun fromJson(json: String) = mapper.readValue<", name, ">(json)"); - }); - } - ); - } - - private jacksonRenameAttribute( - propName: Name, - jsonName: string, - required: boolean, - ignore = false - ): Sourcelike | undefined { - const escapedName = stringEscape(jsonName); - const namesDiffer = this.sourcelikeToString(propName) !== escapedName; - const properties: Sourcelike[] = []; - const isPrefixBool = jsonName.startsWith("is"); // https://github.com/FasterXML/jackson-module-kotlin/issues/80 - const propertyOpts: Sourcelike[] = []; - - if (namesDiffer || isPrefixBool) { - propertyOpts.push('"' + escapedName + '"'); - } - - if (required) { - propertyOpts.push("required=true"); - } - - if (propertyOpts.length > 0) { - properties.push(["@get:JsonProperty(", arrayIntercalate(", ", propertyOpts), ")"]); - properties.push(["@field:JsonProperty(", arrayIntercalate(", ", propertyOpts), ")"]); - } - - if (ignore) { - properties.push("@get:JsonIgnore"); - properties.push("@field:JsonIgnore"); - } - - return properties.length === 0 ? undefined : properties; - } - - protected emitEmptyClassDefinition(c: ClassType, className: Name): void { - this.emitDescription(this.descriptionForType(c)); - - this.emitLine("typealias ", className, " = JsonNode"); - } - - protected emitClassDefinitionMethods(c: ClassType, className: Name): void { - const isTopLevel = iterableSome(this.topLevels, ([_, top]) => top === c); - if (isTopLevel) { - this.emitBlock(")", () => { - this.emitLine("fun toJson() = mapper.writeValueAsString(this)"); - this.ensureBlankLine(); - this.emitBlock("companion object", () => { - this.emitLine("fun fromJson(json: String) = mapper.readValue<", className, ">(json)"); - }); - }); - } else { - this.emitLine(")"); - } - } - - protected renameAttribute(name: Name, jsonName: string, required: boolean, meta: Array<() => void>): void { - const rename = this.jacksonRenameAttribute(name, jsonName, required); - if (rename !== undefined) { - meta.push(() => this.emitLine(rename)); - } - } - - protected emitEnumDefinition(e: EnumType, enumName: Name): void { - this.emitDescription(this.descriptionForType(e)); - - this.emitBlock(["enum class ", enumName, "(val value: String)"], () => { - let count = e.cases.size; - this.forEachEnumCase(e, "none", (name, json) => { - this.emitLine(name, `("${stringEscape(json)}")`, --count === 0 ? ";" : ","); - }); - this.ensureBlankLine(); - this.emitBlock("companion object", () => { - this.emitBlock(["fun fromValue(value: String): ", enumName, " = when (value)"], () => { - let table: Sourcelike[][] = []; - this.forEachEnumCase(e, "none", (name, json) => { - table.push([[`"${stringEscape(json)}"`], [" -> ", name]]); - }); - table.push([["else"], [" -> throw IllegalArgumentException()"]]); - this.emitTable(table); - }); - }); - }); - } - - private emitGenericConverter(): void { - this.ensureBlankLine(); - this.emitMultiline(` -@Suppress("UNCHECKED_CAST") -private fun ObjectMapper.convert(k: kotlin.reflect.KClass<*>, fromJson: (JsonNode) -> T, toJson: (T) -> String, isUnion: Boolean = false) = registerModule(SimpleModule().apply { - addSerializer(k.java as Class, object : StdSerializer(k.java as Class) { - override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) = gen.writeRawValue(toJson(value)) - }) - addDeserializer(k.java as Class, object : StdDeserializer(k.java as Class) { - override fun deserialize(p: JsonParser, ctxt: DeserializationContext) = fromJson(p.readValueAsTree()) - }) -})`); - } - - protected emitUnionDefinitionMethods( - u: UnionType, - nonNulls: ReadonlySet, - maybeNull: PrimitiveType | null, - unionName: Name - ): void { - this.ensureBlankLine(); - this.emitLine("fun toJson(): String = mapper.writeValueAsString(when (this) {"); - this.indent(() => { - let toJsonTable: Sourcelike[][] = []; - this.forEachUnionMember(u, nonNulls, "none", null, name => { - toJsonTable.push([["is ", name], [" -> this.value"]]); - }); - if (maybeNull !== null) { - const name = this.nameForUnionMember(u, maybeNull); - toJsonTable.push([["is ", name], [' -> "null"']]); - } - - this.emitTable(toJsonTable); - }); - this.emitLine("})"); - this.ensureBlankLine(); - this.emitBlock("companion object", () => { - this.emitLine("fun fromJson(jn: JsonNode): ", unionName, " = when (jn) {"); - this.indent(() => { - let table: Sourcelike[][] = []; - this.forEachUnionMember(u, nonNulls, "none", null, (name, t) => { - table.push([[this.unionMemberJsonValueGuard(t, "jn")], [" -> ", name, "(mapper.treeToValue(jn))"]]); - }); - if (maybeNull !== null) { - const name = this.nameForUnionMember(u, maybeNull); - table.push([[this.unionMemberJsonValueGuard(maybeNull, "jn")], [" -> ", name, "()"]]); - } - - table.push([["else"], [" -> throw IllegalArgumentException()"]]); - this.emitTable(table); - }); - this.emitLine("}"); - }); - } -} - -/** - * Currently supports simple classes, enums, and TS string unions (which are also enums). - * TODO: Union, Any, Top Level Array, Top Level Map - */ -export class KotlinXRenderer extends KotlinRenderer { - public constructor( - targetLanguage: TargetLanguage, - renderContext: RenderContext, - _kotlinOptions: OptionValues - ) { - super(targetLanguage, renderContext, _kotlinOptions); - } - - protected anySourceType(optional: string): Sourcelike { - return ["JsonElement", optional]; - } - - protected arrayType(arrayType: ArrayType, withIssues = false, noOptional = false): Sourcelike { - const valType = this.kotlinType(arrayType.items, withIssues, true); - const name = this.sourcelikeToString(valType); - if (name === "JsonObject" || name === "JsonElement") { - return "JsonArray"; - } - - return super.arrayType(arrayType, withIssues, noOptional); - } - - protected mapType(mapType: MapType, withIssues = false, noOptional = false): Sourcelike { - const valType = this.kotlinType(mapType.values, withIssues, true); - const name = this.sourcelikeToString(valType); - if (name === "JsonObject" || name === "JsonElement") { - return "JsonObject"; - } - - return super.mapType(mapType, withIssues, noOptional); - } - - protected emitTopLevelMap(t: MapType, name: Name): void { - const elementType = this.kotlinType(t.values); - if (elementType === "JsonObject") { - this.emitLine(["typealias ", name, " = JsonObject"]); - } else { - super.emitTopLevelMap(t, name); - } - } - - protected emitTopLevelArray(t: ArrayType, name: Name): void { - const elementType = this.kotlinType(t.items); - this.emitLine(["typealias ", name, " = JsonArray<", elementType, ">"]); - } - - protected emitUsageHeader(): void { - this.emitLine("// To parse the JSON, install kotlin's serialization plugin and do:"); - this.emitLine("//"); - const table: Sourcelike[][] = []; - table.push(["// val ", "json", " = Json { allowStructuredMapKeys = true }"]); - this.forEachTopLevel("none", (_, name) => { - table.push([ - "// val ", - modifySource(camelCase, name), - ` = json.parse(${this.sourcelikeToString(name)}.serializer(), jsonString)` - ]); - }); - this.emitTable(table); - } - - protected emitHeader(): void { - super.emitHeader(); - - this.emitLine("import kotlinx.serialization.*"); - this.emitLine("import kotlinx.serialization.json.*"); - this.emitLine("import kotlinx.serialization.descriptors.*"); - this.emitLine("import kotlinx.serialization.encoding.*"); - } - - protected emitClassAnnotations(_c: Type, _className: Name): void { - this.emitLine("@Serializable"); - } - - protected renameAttribute(name: Name, jsonName: string, _required: boolean, meta: Array<() => void>): void { - const rename = this._rename(name, jsonName); - if (rename !== undefined) { - meta.push(() => this.emitLine(rename)); - } - } - - private _rename(propName: Name, jsonName: string): Sourcelike | undefined { - const escapedName = stringEscape(jsonName); - const namesDiffer = this.sourcelikeToString(propName) !== escapedName; - if (namesDiffer) { - return ['@SerialName("', escapedName, '")']; - } - - return undefined; - } - - protected emitEnumDefinition(e: EnumType, enumName: Name): void { - this.emitDescription(this.descriptionForType(e)); - - this.emitLine(["@Serializable"]); - this.emitBlock(["enum class ", enumName, "(val value: String)"], () => { - let count = e.cases.size; - this.forEachEnumCase(e, "none", (name, json) => { - const jsonEnum = stringEscape(json); - this.emitLine(`@SerialName("${jsonEnum}") `, name, `("${jsonEnum}")`, --count === 0 ? ";" : ","); - }); - }); - } -} diff --git a/packages/quicktype-core/src/language/Kotlin/KotlinJacksonRenderer.ts b/packages/quicktype-core/src/language/Kotlin/KotlinJacksonRenderer.ts new file mode 100644 index 000000000..0b840946a --- /dev/null +++ b/packages/quicktype-core/src/language/Kotlin/KotlinJacksonRenderer.ts @@ -0,0 +1,293 @@ +import { arrayIntercalate, iterableSome } from "collection-utils"; + +import { type Name } from "../../Naming"; +import { type RenderContext } from "../../Renderer"; +import { type OptionValues } from "../../RendererOptions"; +import { type Sourcelike, modifySource } from "../../Source"; +import { camelCase } from "../../support/Strings"; +import { mustNotHappen } from "../../support/Support"; +import { type TargetLanguage } from "../../TargetLanguage"; +import { + type ArrayType, + ClassType, + type EnumType, + type MapType, + type PrimitiveType, + type Type, + UnionType +} from "../../Type"; +import { matchType, nullableFromUnion } from "../../TypeUtils"; + +import { KotlinRenderer } from "./KotlinRenderer"; +import { type kotlinOptions } from "./language"; +import { stringEscape } from "./utils"; + +export class KotlinJacksonRenderer extends KotlinRenderer { + public constructor( + targetLanguage: TargetLanguage, + renderContext: RenderContext, + _kotlinOptions: OptionValues + ) { + super(targetLanguage, renderContext, _kotlinOptions); + } + + private unionMemberJsonValueGuard(t: Type, _e: Sourcelike): Sourcelike { + return matchType( + t, + _anyType => "is Any", + _nullType => "null", + _boolType => "is BooleanNode", + _integerType => "is IntNode, is LongNode", + _doubleType => "is DoubleNode", + _stringType => "is TextNode", + _arrayType => "is ArrayNode", + // These could be stricter, but for now we don't allow maps + // and objects in the same union + _classType => "is ObjectNode", + _mapType => "is ObjectNode", + // This could be stricter, but for now we don't allow strings + // and enums in the same union + _enumType => "is TextNode", + _unionType => mustNotHappen() + ); + } + + protected emitUsageHeader(): void { + this.emitLine("// To parse the JSON, install jackson-module-kotlin and do:"); + this.emitLine("//"); + this.forEachTopLevel("none", (_, name) => { + this.emitLine("// val ", modifySource(camelCase, name), " = ", name, ".fromJson(jsonString)"); + }); + } + + protected emitHeader(): void { + super.emitHeader(); + + this.emitMultiline(`import com.fasterxml.jackson.annotation.* +import com.fasterxml.jackson.core.* +import com.fasterxml.jackson.databind.* +import com.fasterxml.jackson.databind.deser.std.StdDeserializer +import com.fasterxml.jackson.databind.module.SimpleModule +import com.fasterxml.jackson.databind.node.* +import com.fasterxml.jackson.databind.ser.std.StdSerializer +import com.fasterxml.jackson.module.kotlin.*`); + + const hasUnions = iterableSome( + this.typeGraph.allNamedTypes(), + t => t instanceof UnionType && nullableFromUnion(t) === null + ); + const hasEmptyObjects = iterableSome( + this.typeGraph.allNamedTypes(), + c => c instanceof ClassType && c.getProperties().size === 0 + ); + if (hasUnions || this.haveEnums || hasEmptyObjects) { + this.emitGenericConverter(); + } + + let converters: Sourcelike[][] = []; + // if (hasEmptyObjects) { + // converters.push([["convert(JsonNode::class,"], [" { it },"], [" { writeValueAsString(it) })"]]); + // } + this.forEachEnum("none", (_, name) => { + converters.push([ + ["convert(", name, "::class,"], + [" { ", name, ".fromValue(it.asText()) },"], + [' { "\\"${it.value}\\"" })'] + ]); + }); + this.forEachUnion("none", (_, name) => { + converters.push([ + ["convert(", name, "::class,"], + [" { ", name, ".fromJson(it) },"], + [" { it.toJson() }, true)"] + ]); + }); + + this.ensureBlankLine(); + this.emitLine("val mapper = jacksonObjectMapper().apply {"); + this.indent(() => { + this.emitLine("propertyNamingStrategy = PropertyNamingStrategy.LOWER_CAMEL_CASE"); + this.emitLine("setSerializationInclusion(JsonInclude.Include.NON_NULL)"); + }); + + if (converters.length > 0) { + this.indent(() => this.emitTable(converters)); + } + + this.emitLine("}"); + } + + protected emitTopLevelArray(t: ArrayType, name: Name): void { + const elementType = this.kotlinType(t.items); + this.emitBlock( + ["class ", name, "(elements: Collection<", elementType, ">) : ArrayList<", elementType, ">(elements)"], + () => { + this.emitLine("fun toJson() = mapper.writeValueAsString(this)"); + this.ensureBlankLine(); + this.emitBlock("companion object", () => { + this.emitLine("fun fromJson(json: String) = mapper.readValue<", name, ">(json)"); + }); + } + ); + } + + protected emitTopLevelMap(t: MapType, name: Name): void { + const elementType = this.kotlinType(t.values); + this.emitBlock( + [ + "class ", + name, + "(elements: Map) : HashMap(elements)" + ], + () => { + this.emitLine("fun toJson() = mapper.writeValueAsString(this)"); + this.ensureBlankLine(); + this.emitBlock("companion object", () => { + this.emitLine("fun fromJson(json: String) = mapper.readValue<", name, ">(json)"); + }); + } + ); + } + + private jacksonRenameAttribute( + propName: Name, + jsonName: string, + required: boolean, + ignore = false + ): Sourcelike | undefined { + const escapedName = stringEscape(jsonName); + const namesDiffer = this.sourcelikeToString(propName) !== escapedName; + const properties: Sourcelike[] = []; + const isPrefixBool = jsonName.startsWith("is"); // https://github.com/FasterXML/jackson-module-kotlin/issues/80 + const propertyOpts: Sourcelike[] = []; + + if (namesDiffer || isPrefixBool) { + propertyOpts.push('"' + escapedName + '"'); + } + + if (required) { + propertyOpts.push("required=true"); + } + + if (propertyOpts.length > 0) { + properties.push(["@get:JsonProperty(", arrayIntercalate(", ", propertyOpts), ")"]); + properties.push(["@field:JsonProperty(", arrayIntercalate(", ", propertyOpts), ")"]); + } + + if (ignore) { + properties.push("@get:JsonIgnore"); + properties.push("@field:JsonIgnore"); + } + + return properties.length === 0 ? undefined : properties; + } + + protected emitEmptyClassDefinition(c: ClassType, className: Name): void { + this.emitDescription(this.descriptionForType(c)); + + this.emitLine("typealias ", className, " = JsonNode"); + } + + protected emitClassDefinitionMethods(c: ClassType, className: Name): void { + const isTopLevel = iterableSome(this.topLevels, ([_, top]) => top === c); + if (isTopLevel) { + this.emitBlock(")", () => { + this.emitLine("fun toJson() = mapper.writeValueAsString(this)"); + this.ensureBlankLine(); + this.emitBlock("companion object", () => { + this.emitLine("fun fromJson(json: String) = mapper.readValue<", className, ">(json)"); + }); + }); + } else { + this.emitLine(")"); + } + } + + protected renameAttribute(name: Name, jsonName: string, required: boolean, meta: Array<() => void>): void { + const rename = this.jacksonRenameAttribute(name, jsonName, required); + if (rename !== undefined) { + meta.push(() => this.emitLine(rename)); + } + } + + protected emitEnumDefinition(e: EnumType, enumName: Name): void { + this.emitDescription(this.descriptionForType(e)); + + this.emitBlock(["enum class ", enumName, "(val value: String)"], () => { + let count = e.cases.size; + this.forEachEnumCase(e, "none", (name, json) => { + this.emitLine(name, `("${stringEscape(json)}")`, --count === 0 ? ";" : ","); + }); + this.ensureBlankLine(); + this.emitBlock("companion object", () => { + this.emitBlock(["fun fromValue(value: String): ", enumName, " = when (value)"], () => { + let table: Sourcelike[][] = []; + this.forEachEnumCase(e, "none", (name, json) => { + table.push([[`"${stringEscape(json)}"`], [" -> ", name]]); + }); + table.push([["else"], [" -> throw IllegalArgumentException()"]]); + this.emitTable(table); + }); + }); + }); + } + + private emitGenericConverter(): void { + this.ensureBlankLine(); + this.emitMultiline(` +@Suppress("UNCHECKED_CAST") +private fun ObjectMapper.convert(k: kotlin.reflect.KClass<*>, fromJson: (JsonNode) -> T, toJson: (T) -> String, isUnion: Boolean = false) = registerModule(SimpleModule().apply { + addSerializer(k.java as Class, object : StdSerializer(k.java as Class) { + override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) = gen.writeRawValue(toJson(value)) + }) + addDeserializer(k.java as Class, object : StdDeserializer(k.java as Class) { + override fun deserialize(p: JsonParser, ctxt: DeserializationContext) = fromJson(p.readValueAsTree()) + }) +})`); + } + + protected emitUnionDefinitionMethods( + u: UnionType, + nonNulls: ReadonlySet, + maybeNull: PrimitiveType | null, + unionName: Name + ): void { + this.ensureBlankLine(); + this.emitLine("fun toJson(): String = mapper.writeValueAsString(when (this) {"); + this.indent(() => { + let toJsonTable: Sourcelike[][] = []; + this.forEachUnionMember(u, nonNulls, "none", null, name => { + toJsonTable.push([["is ", name], [" -> this.value"]]); + }); + if (maybeNull !== null) { + const name = this.nameForUnionMember(u, maybeNull); + toJsonTable.push([["is ", name], [' -> "null"']]); + } + + this.emitTable(toJsonTable); + }); + this.emitLine("})"); + this.ensureBlankLine(); + this.emitBlock("companion object", () => { + this.emitLine("fun fromJson(jn: JsonNode): ", unionName, " = when (jn) {"); + this.indent(() => { + let table: Sourcelike[][] = []; + this.forEachUnionMember(u, nonNulls, "none", null, (name, t) => { + table.push([[this.unionMemberJsonValueGuard(t, "jn")], [" -> ", name, "(mapper.treeToValue(jn))"]]); + }); + if (maybeNull !== null) { + const name = this.nameForUnionMember(u, maybeNull); + table.push([[this.unionMemberJsonValueGuard(maybeNull, "jn")], [" -> ", name, "()"]]); + } + + table.push([["else"], [" -> throw IllegalArgumentException()"]]); + this.emitTable(table); + }); + this.emitLine("}"); + }); + } +} diff --git a/packages/quicktype-core/src/language/Kotlin/KotlinKlaxonRenderer.ts b/packages/quicktype-core/src/language/Kotlin/KotlinKlaxonRenderer.ts new file mode 100644 index 000000000..bcfaf9043 --- /dev/null +++ b/packages/quicktype-core/src/language/Kotlin/KotlinKlaxonRenderer.ts @@ -0,0 +1,306 @@ +import { arrayIntercalate, iterableSome } from "collection-utils"; + +import { type Name } from "../../Naming"; +import { type RenderContext } from "../../Renderer"; +import { type OptionValues } from "../../RendererOptions"; +import { type Sourcelike, modifySource } from "../../Source"; +import { camelCase } from "../../support/Strings"; +import { mustNotHappen } from "../../support/Support"; +import { type TargetLanguage } from "../../TargetLanguage"; +import { + type ArrayType, + ClassType, + type EnumType, + type MapType, + type PrimitiveType, + type Type, + UnionType +} from "../../Type"; +import { matchType, nullableFromUnion } from "../../TypeUtils"; + +import { KotlinRenderer } from "./KotlinRenderer"; +import { type kotlinOptions } from "./language"; +import { stringEscape } from "./utils"; + +export class KotlinKlaxonRenderer extends KotlinRenderer { + public constructor( + targetLanguage: TargetLanguage, + renderContext: RenderContext, + _kotlinOptions: OptionValues + ) { + super(targetLanguage, renderContext, _kotlinOptions); + } + + private unionMemberFromJsonValue(t: Type, e: Sourcelike): Sourcelike { + return matchType( + t, + _anyType => [e, ".inside"], + _nullType => "null", + _boolType => [e, ".boolean"], + _integerType => ["(", e, ".int?.toLong() ?: ", e, ".longValue)"], + _doubleType => [e, ".double"], + _stringType => [e, ".string"], + arrayType => [e, ".array?.let { klaxon.parseFromJsonArray<", this.kotlinType(arrayType.items), ">(it) }"], + _classType => [e, ".obj?.let { klaxon.parseFromJsonObject<", this.kotlinType(t), ">(it) }"], + _mapType => [e, ".obj?.let { klaxon.parseFromJsonObject<", this.kotlinType(t), ">(it) }"], + enumType => [e, ".string?.let { ", this.kotlinType(enumType), ".fromValue(it) }"], + _unionType => mustNotHappen() + ); + } + + private unionMemberJsonValueGuard(t: Type, _e: Sourcelike): Sourcelike { + return matchType( + t, + _anyType => "is Any", + _nullType => "null", + _boolType => "is Boolean", + _integerType => "is Int, is Long", + _doubleType => "is Double", + _stringType => "is String", + _arrayType => "is JsonArray<*>", + // These could be stricter, but for now we don't allow maps + // and objects in the same union + _classType => "is JsonObject", + _mapType => "is JsonObject", + // This could be stricter, but for now we don't allow strings + // and enums in the same union + _enumType => "is String", + _unionType => mustNotHappen() + ); + } + + protected emitUsageHeader(): void { + this.emitLine("// To parse the JSON, install Klaxon and do:"); + this.emitLine("//"); + this.forEachTopLevel("none", (_, name) => { + this.emitLine("// val ", modifySource(camelCase, name), " = ", name, ".fromJson(jsonString)"); + }); + } + + protected emitHeader(): void { + super.emitHeader(); + + this.emitLine("import com.beust.klaxon.*"); + + const hasUnions = iterableSome( + this.typeGraph.allNamedTypes(), + t => t instanceof UnionType && nullableFromUnion(t) === null + ); + const hasEmptyObjects = iterableSome( + this.typeGraph.allNamedTypes(), + c => c instanceof ClassType && c.getProperties().size === 0 + ); + if (hasUnions || this.haveEnums || hasEmptyObjects) { + this.emitGenericConverter(); + } + + let converters: Sourcelike[][] = []; + if (hasEmptyObjects) { + converters.push([[".convert(JsonObject::class,"], [" { it.obj!! },"], [" { it.toJsonString() })"]]); + } + + this.forEachEnum("none", (_, name) => { + converters.push([ + [".convert(", name, "::class,"], + [" { ", name, ".fromValue(it.string!!) },"], + [' { "\\"${it.value}\\"" })'] + ]); + }); + this.forEachUnion("none", (_, name) => { + converters.push([ + [".convert(", name, "::class,"], + [" { ", name, ".fromJson(it) },"], + [" { it.toJson() }, true)"] + ]); + }); + + this.ensureBlankLine(); + this.emitLine("private val klaxon = Klaxon()"); + if (converters.length > 0) { + this.indent(() => this.emitTable(converters)); + } + } + + protected emitTopLevelArray(t: ArrayType, name: Name): void { + const elementType = this.kotlinType(t.items); + this.emitBlock( + ["class ", name, "(elements: Collection<", elementType, ">) : ArrayList<", elementType, ">(elements)"], + () => { + this.emitLine("public fun toJson() = klaxon.toJsonString(this)"); + this.ensureBlankLine(); + this.emitBlock("companion object", () => { + this.emitLine( + "public fun fromJson(json: String) = ", + name, + "(klaxon.parseArray<", + elementType, + ">(json)!!)" + ); + }); + } + ); + } + + protected emitTopLevelMap(t: MapType, name: Name): void { + const elementType = this.kotlinType(t.values); + this.emitBlock( + [ + "class ", + name, + "(elements: Map) : HashMap(elements)" + ], + () => { + this.emitLine("public fun toJson() = klaxon.toJsonString(this)"); + this.ensureBlankLine(); + this.emitBlock("companion object", () => { + this.emitBlock( + ["public fun fromJson(json: String) = ", name], + () => { + this.emitLine( + "klaxon.parseJsonObject(java.io.StringReader(json)) as Map" + ); + }, + "paren" + ); + }); + } + ); + } + + private klaxonRenameAttribute(propName: Name, jsonName: string, ignore = false): Sourcelike | undefined { + const escapedName = stringEscape(jsonName); + const namesDiffer = this.sourcelikeToString(propName) !== escapedName; + const properties: Sourcelike[] = []; + if (namesDiffer) { + properties.push(['name = "', escapedName, '"']); + } + + if (ignore) { + properties.push("ignored = true"); + } + + return properties.length === 0 ? undefined : ["@Json(", arrayIntercalate(", ", properties), ")"]; + } + + protected emitEmptyClassDefinition(c: ClassType, className: Name): void { + this.emitDescription(this.descriptionForType(c)); + + this.emitLine("typealias ", className, " = JsonObject"); + } + + protected emitClassDefinitionMethods(c: ClassType, className: Name): void { + const isTopLevel = iterableSome(this.topLevels, ([_, top]) => top === c); + if (isTopLevel) { + this.emitBlock(")", () => { + this.emitLine("public fun toJson() = klaxon.toJsonString(this)"); + this.ensureBlankLine(); + this.emitBlock("companion object", () => { + this.emitLine("public fun fromJson(json: String) = klaxon.parse<", className, ">(json)"); + }); + }); + } else { + this.emitLine(")"); + } + } + + protected renameAttribute(name: Name, jsonName: string, _required: boolean, meta: Array<() => void>): void { + const rename = this.klaxonRenameAttribute(name, jsonName); + if (rename !== undefined) { + meta.push(() => this.emitLine(rename)); + } + } + + protected emitEnumDefinition(e: EnumType, enumName: Name): void { + this.emitDescription(this.descriptionForType(e)); + + this.emitBlock(["enum class ", enumName, "(val value: String)"], () => { + let count = e.cases.size; + this.forEachEnumCase(e, "none", (name, json) => { + this.emitLine(name, `("${stringEscape(json)}")`, --count === 0 ? ";" : ","); + }); + this.ensureBlankLine(); + this.emitBlock("companion object", () => { + this.emitBlock(["public fun fromValue(value: String): ", enumName, " = when (value)"], () => { + let table: Sourcelike[][] = []; + this.forEachEnumCase(e, "none", (name, json) => { + table.push([[`"${stringEscape(json)}"`], [" -> ", name]]); + }); + table.push([["else"], [" -> throw IllegalArgumentException()"]]); + this.emitTable(table); + }); + }); + }); + } + + private emitGenericConverter(): void { + this.ensureBlankLine(); + this.emitLine( + "private fun Klaxon.convert(k: kotlin.reflect.KClass<*>, fromJson: (JsonValue) -> T, toJson: (T) -> String, isUnion: Boolean = false) =" + ); + this.indent(() => { + this.emitLine("this.converter(object: Converter {"); + this.indent(() => { + this.emitLine('@Suppress("UNCHECKED_CAST")'); + this.emitTable([ + ["override fun toJson(value: Any)", " = toJson(value as T)"], + ["override fun fromJson(jv: JsonValue)", " = fromJson(jv) as Any"], + [ + "override fun canConvert(cls: Class<*>)", + " = cls == k.java || (isUnion && cls.superclass == k.java)" + ] + ]); + }); + this.emitLine("})"); + }); + } + + protected emitUnionDefinitionMethods( + u: UnionType, + nonNulls: ReadonlySet, + maybeNull: PrimitiveType | null, + unionName: Name + ): void { + this.ensureBlankLine(); + this.emitLine("public fun toJson(): String = klaxon.toJsonString(when (this) {"); + this.indent(() => { + let toJsonTable: Sourcelike[][] = []; + this.forEachUnionMember(u, nonNulls, "none", null, name => { + toJsonTable.push([["is ", name], [" -> this.value"]]); + }); + if (maybeNull !== null) { + const name = this.nameForUnionMember(u, maybeNull); + toJsonTable.push([["is ", name], [' -> "null"']]); + } + + this.emitTable(toJsonTable); + }); + this.emitLine("})"); + this.ensureBlankLine(); + this.emitBlock("companion object", () => { + this.emitLine("public fun fromJson(jv: JsonValue): ", unionName, " = when (jv.inside) {"); + this.indent(() => { + let table: Sourcelike[][] = []; + this.forEachUnionMember(u, nonNulls, "none", null, (name, t) => { + table.push([ + [this.unionMemberJsonValueGuard(t, "jv.inside")], + [" -> ", name, "(", this.unionMemberFromJsonValue(t, "jv"), "!!)"] + ]); + }); + if (maybeNull !== null) { + const name = this.nameForUnionMember(u, maybeNull); + table.push([[this.unionMemberJsonValueGuard(maybeNull, "jv.inside")], [" -> ", name, "()"]]); + } + + table.push([["else"], [" -> throw IllegalArgumentException()"]]); + this.emitTable(table); + }); + this.emitLine("}"); + }); + } +} diff --git a/packages/quicktype-core/src/language/Kotlin/KotlinRenderer.ts b/packages/quicktype-core/src/language/Kotlin/KotlinRenderer.ts new file mode 100644 index 000000000..d7e5abaaa --- /dev/null +++ b/packages/quicktype-core/src/language/Kotlin/KotlinRenderer.ts @@ -0,0 +1,300 @@ +import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../../Annotation"; +import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../../ConvenienceRenderer"; +import { type Name, type Namer, funPrefixNamer } from "../../Naming"; +import { type RenderContext } from "../../Renderer"; +import { type OptionValues } from "../../RendererOptions"; +import { type Sourcelike, maybeAnnotated } from "../../Source"; +import { acronymStyle } from "../../support/Acronyms"; +import { type TargetLanguage } from "../../TargetLanguage"; +import { + ArrayType, + type ClassProperty, + type ClassType, + type EnumType, + MapType, + type ObjectType, + type PrimitiveType, + type Type, + type UnionType +} from "../../Type"; +import { matchType, nullableFromUnion, removeNullFromUnion } from "../../TypeUtils"; + +import { keywords } from "./constants"; +import { type kotlinOptions } from "./language"; +import { kotlinNameStyle } from "./utils"; + +export class KotlinRenderer extends ConvenienceRenderer { + public constructor( + targetLanguage: TargetLanguage, + renderContext: RenderContext, + protected readonly _kotlinOptions: OptionValues + ) { + super(targetLanguage, renderContext); + } + + protected forbiddenNamesForGlobalNamespace(): readonly string[] { + return keywords; + } + + protected forbiddenForObjectProperties(_o: ObjectType, _classNamed: Name): ForbiddenWordsInfo { + return { names: [], includeGlobalForbidden: true }; + } + + protected forbiddenForEnumCases(_e: EnumType, _enumName: Name): ForbiddenWordsInfo { + return { names: [], includeGlobalForbidden: true }; + } + + protected forbiddenForUnionMembers(_u: UnionType, _unionName: Name): ForbiddenWordsInfo { + return { names: [], includeGlobalForbidden: false }; + } + + protected topLevelNameStyle(rawName: string): string { + return kotlinNameStyle(true, rawName); + } + + protected makeNamedTypeNamer(): Namer { + return funPrefixNamer("upper", s => kotlinNameStyle(true, s, acronymStyle(this._kotlinOptions.acronymStyle))); + } + + protected namerForObjectProperty(): Namer { + return funPrefixNamer("lower", s => kotlinNameStyle(false, s, acronymStyle(this._kotlinOptions.acronymStyle))); + } + + protected makeUnionMemberNamer(): Namer { + return funPrefixNamer("upper", s => kotlinNameStyle(true, s) + "Value"); + } + + protected makeEnumCaseNamer(): Namer { + return funPrefixNamer("upper", s => kotlinNameStyle(true, s, acronymStyle(this._kotlinOptions.acronymStyle))); + } + + protected emitDescriptionBlock(lines: Sourcelike[]): void { + this.emitCommentLines(lines, { lineStart: " * ", beforeComment: "/**", afterComment: " */" }); + } + + protected emitBlock(line: Sourcelike, f: () => void, delimiter: "curly" | "paren" | "lambda" = "curly"): void { + const [open, close] = delimiter === "curly" ? ["{", "}"] : delimiter === "paren" ? ["(", ")"] : ["{", "})"]; + this.emitLine(line, " ", open); + this.indent(f); + this.emitLine(close); + } + + protected anySourceType(optional: string): Sourcelike { + return ["Any", optional]; + } + + // (asarazan): I've broken out the following two functions + // because some renderers, such as kotlinx, can cope with `any`, while some get mad. + protected arrayType(arrayType: ArrayType, withIssues = false, _noOptional = false): Sourcelike { + return ["List<", this.kotlinType(arrayType.items, withIssues), ">"]; + } + + protected mapType(mapType: MapType, withIssues = false, _noOptional = false): Sourcelike { + return ["Map"]; + } + + protected kotlinType(t: Type, withIssues = false, noOptional = false): Sourcelike { + const optional = noOptional ? "" : "?"; + return matchType( + t, + _anyType => { + return maybeAnnotated(withIssues, anyTypeIssueAnnotation, this.anySourceType(optional)); + }, + _nullType => { + return maybeAnnotated(withIssues, nullTypeIssueAnnotation, this.anySourceType(optional)); + }, + _boolType => "Boolean", + _integerType => "Long", + _doubleType => "Double", + _stringType => "String", + arrayType => this.arrayType(arrayType, withIssues), + classType => this.nameForNamedType(classType), + mapType => this.mapType(mapType, withIssues), + enumType => this.nameForNamedType(enumType), + unionType => { + const nullable = nullableFromUnion(unionType); + if (nullable !== null) return [this.kotlinType(nullable, withIssues), optional]; + return this.nameForNamedType(unionType); + } + ); + } + + protected emitUsageHeader(): void { + // To be overridden + } + + protected emitHeader(): void { + if (this.leadingComments !== undefined) { + this.emitComments(this.leadingComments); + } else { + this.emitUsageHeader(); + } + + this.ensureBlankLine(); + this.emitLine("package ", this._kotlinOptions.packageName); + this.ensureBlankLine(); + } + + protected emitTopLevelPrimitive(t: PrimitiveType, name: Name): void { + const elementType = this.kotlinType(t); + this.emitLine(["typealias ", name, " = ", elementType, ""]); + } + + protected emitTopLevelArray(t: ArrayType, name: Name): void { + const elementType = this.kotlinType(t.items); + this.emitLine(["typealias ", name, " = ArrayList<", elementType, ">"]); + } + + protected emitTopLevelMap(t: MapType, name: Name): void { + const elementType = this.kotlinType(t.values); + this.emitLine(["typealias ", name, " = HashMap"]); + } + + protected emitEmptyClassDefinition(c: ClassType, className: Name): void { + this.emitDescription(this.descriptionForType(c)); + this.emitClassAnnotations(c, className); + this.emitLine("class ", className, "()"); + } + + protected emitClassDefinition(c: ClassType, className: Name): void { + if (c.getProperties().size === 0) { + this.emitEmptyClassDefinition(c, className); + return; + } + + const kotlinType = (p: ClassProperty): Sourcelike => { + if (p.isOptional) { + return [this.kotlinType(p.type, true, true), "?"]; + } else { + return this.kotlinType(p.type, true); + } + }; + + this.emitDescription(this.descriptionForType(c)); + this.emitClassAnnotations(c, className); + this.emitLine("data class ", className, " ("); + this.indent(() => { + let count = c.getProperties().size; + let first = true; + this.forEachClassProperty(c, "none", (name, jsonName, p) => { + const nullable = p.type.kind === "union" && nullableFromUnion(p.type as UnionType) !== null; + const nullableOrOptional = p.isOptional || p.type.kind === "null" || nullable; + const last = --count === 0; + let meta: Array<() => void> = []; + + const description = this.descriptionForClassProperty(c, jsonName); + if (description !== undefined) { + meta.push(() => this.emitDescription(description)); + } + + this.renameAttribute(name, jsonName, !nullableOrOptional, meta); + + if (meta.length > 0 && !first) { + this.ensureBlankLine(); + } + + for (const emit of meta) { + emit(); + } + + this.emitLine("val ", name, ": ", kotlinType(p), nullableOrOptional ? " = null" : "", last ? "" : ","); + + if (meta.length > 0 && !last) { + this.ensureBlankLine(); + } + + first = false; + }); + }); + + this.emitClassDefinitionMethods(c, className); + } + + protected emitClassDefinitionMethods(_c: ClassType, _className: Name): void { + this.emitLine(")"); + } + + protected emitClassAnnotations(_c: Type, _className: Name): void { + // to be overridden + } + + protected renameAttribute(_name: Name, _jsonName: string, _required: boolean, _meta: Array<() => void>): void { + // to be overridden + } + + protected emitEnumDefinition(e: EnumType, enumName: Name): void { + this.emitDescription(this.descriptionForType(e)); + + this.emitBlock(["enum class ", enumName], () => { + let count = e.cases.size; + this.forEachEnumCase(e, "none", name => { + this.emitLine(name, --count === 0 ? "" : ","); + }); + }); + } + + protected emitUnionDefinition(u: UnionType, unionName: Name): void { + function sortBy(t: Type): string { + const kind = t.kind; + if (kind === "class") return kind; + return "_" + kind; + } + + this.emitDescription(this.descriptionForType(u)); + + const [maybeNull, nonNulls] = removeNullFromUnion(u, sortBy); + this.emitClassAnnotations(u, unionName); + this.emitBlock(["sealed class ", unionName], () => { + { + let table: Sourcelike[][] = []; + this.forEachUnionMember(u, nonNulls, "none", null, (name, t) => { + table.push([ + ["class ", name, "(val value: ", this.kotlinType(t), ")"], + [" : ", unionName, "()"] + ]); + }); + if (maybeNull !== null) { + table.push([ + ["class ", this.nameForUnionMember(u, maybeNull), "()"], + [" : ", unionName, "()"] + ]); + } + + this.emitTable(table); + } + + this.emitUnionDefinitionMethods(u, nonNulls, maybeNull, unionName); + }); + } + + protected emitUnionDefinitionMethods( + _u: UnionType, + _nonNulls: ReadonlySet, + _maybeNull: PrimitiveType | null, + _unionName: Name + ): void { + // to be overridden + } + + protected emitSourceStructure(): void { + this.emitHeader(); + + // Top-level arrays, maps + this.forEachTopLevel("leading", (t, name) => { + if (t instanceof ArrayType) { + this.emitTopLevelArray(t, name); + } else if (t instanceof MapType) { + this.emitTopLevelMap(t, name); + } else if (t.isPrimitive()) { + this.emitTopLevelPrimitive(t, name); + } + }); + + this.forEachNamedType( + "leading-and-interposing", + (c: ClassType, n: Name) => this.emitClassDefinition(c, n), + (e, n) => this.emitEnumDefinition(e, n), + (u, n) => this.emitUnionDefinition(u, n) + ); + } +} diff --git a/packages/quicktype-core/src/language/Kotlin/KotlinXRenderer.ts b/packages/quicktype-core/src/language/Kotlin/KotlinXRenderer.ts new file mode 100644 index 000000000..66a579b45 --- /dev/null +++ b/packages/quicktype-core/src/language/Kotlin/KotlinXRenderer.ts @@ -0,0 +1,121 @@ +import { type Name } from "../../Naming"; +import { type RenderContext } from "../../Renderer"; +import { type OptionValues } from "../../RendererOptions"; +import { type Sourcelike, modifySource } from "../../Source"; +import { camelCase } from "../../support/Strings"; +import { type TargetLanguage } from "../../TargetLanguage"; +import { type ArrayType, type EnumType, type MapType, type Type } from "../../Type"; + +import { KotlinRenderer } from "./KotlinRenderer"; +import { type kotlinOptions } from "./language"; +import { stringEscape } from "./utils"; + +/** + * Currently supports simple classes, enums, and TS string unions (which are also enums). + * TODO: Union, Any, Top Level Array, Top Level Map + */ +export class KotlinXRenderer extends KotlinRenderer { + public constructor( + targetLanguage: TargetLanguage, + renderContext: RenderContext, + _kotlinOptions: OptionValues + ) { + super(targetLanguage, renderContext, _kotlinOptions); + } + + protected anySourceType(optional: string): Sourcelike { + return ["JsonElement", optional]; + } + + protected arrayType(arrayType: ArrayType, withIssues = false, noOptional = false): Sourcelike { + const valType = this.kotlinType(arrayType.items, withIssues, true); + const name = this.sourcelikeToString(valType); + if (name === "JsonObject" || name === "JsonElement") { + return "JsonArray"; + } + + return super.arrayType(arrayType, withIssues, noOptional); + } + + protected mapType(mapType: MapType, withIssues = false, noOptional = false): Sourcelike { + const valType = this.kotlinType(mapType.values, withIssues, true); + const name = this.sourcelikeToString(valType); + if (name === "JsonObject" || name === "JsonElement") { + return "JsonObject"; + } + + return super.mapType(mapType, withIssues, noOptional); + } + + protected emitTopLevelMap(t: MapType, name: Name): void { + const elementType = this.kotlinType(t.values); + if (elementType === "JsonObject") { + this.emitLine(["typealias ", name, " = JsonObject"]); + } else { + super.emitTopLevelMap(t, name); + } + } + + protected emitTopLevelArray(t: ArrayType, name: Name): void { + const elementType = this.kotlinType(t.items); + this.emitLine(["typealias ", name, " = JsonArray<", elementType, ">"]); + } + + protected emitUsageHeader(): void { + this.emitLine("// To parse the JSON, install kotlin's serialization plugin and do:"); + this.emitLine("//"); + const table: Sourcelike[][] = []; + table.push(["// val ", "json", " = Json { allowStructuredMapKeys = true }"]); + this.forEachTopLevel("none", (_, name) => { + table.push([ + "// val ", + modifySource(camelCase, name), + ` = json.parse(${this.sourcelikeToString(name)}.serializer(), jsonString)` + ]); + }); + this.emitTable(table); + } + + protected emitHeader(): void { + super.emitHeader(); + + this.emitLine("import kotlinx.serialization.*"); + this.emitLine("import kotlinx.serialization.json.*"); + this.emitLine("import kotlinx.serialization.descriptors.*"); + this.emitLine("import kotlinx.serialization.encoding.*"); + } + + protected emitClassAnnotations(_c: Type, _className: Name): void { + this.emitLine("@Serializable"); + } + + protected renameAttribute(name: Name, jsonName: string, _required: boolean, meta: Array<() => void>): void { + const rename = this._rename(name, jsonName); + if (rename !== undefined) { + meta.push(() => this.emitLine(rename)); + } + } + + private _rename(propName: Name, jsonName: string): Sourcelike | undefined { + const escapedName = stringEscape(jsonName); + const namesDiffer = this.sourcelikeToString(propName) !== escapedName; + if (namesDiffer) { + return ['@SerialName("', escapedName, '")']; + } + + return undefined; + } + + protected emitEnumDefinition(e: EnumType, enumName: Name): void { + this.emitDescription(this.descriptionForType(e)); + + this.emitLine(["@Serializable"]); + this.emitBlock(["enum class ", enumName, "(val value: String)"], () => { + let count = e.cases.size; + this.forEachEnumCase(e, "none", (name, json) => { + const jsonEnum = stringEscape(json); + this.emitLine(`@SerialName("${jsonEnum}") `, name, `("${jsonEnum}")`, --count === 0 ? ";" : ","); + }); + }); + } +} diff --git a/packages/quicktype-core/src/language/Kotlin/constants.ts b/packages/quicktype-core/src/language/Kotlin/constants.ts new file mode 100644 index 000000000..8e83c7388 --- /dev/null +++ b/packages/quicktype-core/src/language/Kotlin/constants.ts @@ -0,0 +1,51 @@ +export const keywords = [ + "package", + "as", + "typealias", + "class", + "this", + "super", + "val", + "var", + "fun", + "for", + "null", + "true", + "false", + "is", + "in", + "throw", + "return", + "break", + "continue", + "object", + "if", + "try", + "else", + "while", + "do", + "when", + "interface", + "typeof", + "klaxon", + "toJson", + "Any", + "Boolean", + "Double", + "Float", + "Long", + "Int", + "Short", + "System", + "Byte", + "String", + "Array", + "List", + "Map", + "Enum", + "Class", + "JsonObject", + "JsonValue", + "Converter", + "Klaxon" +] as const; diff --git a/packages/quicktype-core/src/language/Kotlin/index.ts b/packages/quicktype-core/src/language/Kotlin/index.ts new file mode 100644 index 000000000..e6459d694 --- /dev/null +++ b/packages/quicktype-core/src/language/Kotlin/index.ts @@ -0,0 +1,5 @@ +export { KotlinTargetLanguage, kotlinOptions } from "./language"; +export { KotlinRenderer } from "./KotlinRenderer"; +export { KotlinJacksonRenderer } from "./KotlinJacksonRenderer"; +export { KotlinKlaxonRenderer } from "./KotlinKlaxonRenderer"; +export { KotlinXRenderer } from "./KotlinXRenderer"; diff --git a/packages/quicktype-core/src/language/Kotlin/language.ts b/packages/quicktype-core/src/language/Kotlin/language.ts new file mode 100644 index 000000000..59a150dc9 --- /dev/null +++ b/packages/quicktype-core/src/language/Kotlin/language.ts @@ -0,0 +1,70 @@ +import { type ConvenienceRenderer } from "../../ConvenienceRenderer"; +import { type RenderContext } from "../../Renderer"; +import { EnumOption, type Option, StringOption, getOptionValues } from "../../RendererOptions"; +import { AcronymStyleOptions, acronymOption } from "../../support/Acronyms"; +import { assertNever } from "../../support/Support"; +import { TargetLanguage } from "../../TargetLanguage"; +import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../../types"; + +import { KotlinJacksonRenderer } from "./KotlinJacksonRenderer"; +import { KotlinKlaxonRenderer } from "./KotlinKlaxonRenderer"; +import { KotlinRenderer } from "./KotlinRenderer"; +import { KotlinXRenderer } from "./KotlinXRenderer"; + +export enum Framework { + None = "None", + Jackson = "Jackson", + Klaxon = "Klaxon", + KotlinX = "KotlinX" +} + +export const kotlinOptions = { + framework: new EnumOption( + "framework", + "Serialization framework", + [ + ["just-types", Framework.None], + ["jackson", Framework.Jackson], + ["klaxon", Framework.Klaxon], + ["kotlinx", Framework.KotlinX] + ], + "klaxon" + ), + acronymStyle: acronymOption(AcronymStyleOptions.Pascal), + packageName: new StringOption("package", "Package", "PACKAGE", "quicktype") +}; + +export class KotlinTargetLanguage extends TargetLanguage { + public constructor() { + super("Kotlin", ["kotlin"], "kt"); + } + + protected getOptions(): Array> { + return [kotlinOptions.framework, kotlinOptions.acronymStyle, kotlinOptions.packageName]; + } + + public get supportsOptionalClassProperties(): boolean { + return true; + } + + public get supportsUnionsWithBothNumberTypes(): boolean { + return true; + } + + protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): ConvenienceRenderer { + const options = getOptionValues(kotlinOptions, untypedOptionValues); + + switch (options.framework) { + case Framework.None: + return new KotlinRenderer(this, renderContext, options); + case Framework.Jackson: + return new KotlinJacksonRenderer(this, renderContext, options); + case Framework.Klaxon: + return new KotlinKlaxonRenderer(this, renderContext, options); + case Framework.KotlinX: + return new KotlinXRenderer(this, renderContext, options); + default: + return assertNever(options.framework); + } + } +} diff --git a/packages/quicktype-core/src/language/Kotlin/utils.ts b/packages/quicktype-core/src/language/Kotlin/utils.ts new file mode 100644 index 000000000..69fc8fb0c --- /dev/null +++ b/packages/quicktype-core/src/language/Kotlin/utils.ts @@ -0,0 +1,55 @@ +import { + allLowerWordStyle, + allUpperWordStyle, + combineWords, + escapeNonPrintableMapper, + firstUpperWordStyle, + intToHex, + isDigit, + isLetterOrUnderscore, + isNumeric, + isPrintable, + legalizeCharacters, + splitIntoWords, + utf32ConcatMap +} from "../../support/Strings"; + +function isPartCharacter(codePoint: number): boolean { + return isLetterOrUnderscore(codePoint) || isNumeric(codePoint); +} + +function isStartCharacter(codePoint: number): boolean { + return isPartCharacter(codePoint) && !isDigit(codePoint); +} + +const legalizeName = legalizeCharacters(isPartCharacter); + +export function kotlinNameStyle( + isUpper: boolean, + original: string, + acronymsStyle: (s: string) => string = allUpperWordStyle +): string { + const words = splitIntoWords(original); + return combineWords( + words, + legalizeName, + isUpper ? firstUpperWordStyle : allLowerWordStyle, + firstUpperWordStyle, + isUpper ? allUpperWordStyle : allLowerWordStyle, + acronymsStyle, + "", + isStartCharacter + ); +} + +function unicodeEscape(codePoint: number): string { + return "\\u" + intToHex(codePoint, 4); +} + +// eslint-disable-next-line @typescript-eslint/naming-convention +const _stringEscape = utf32ConcatMap(escapeNonPrintableMapper(isPrintable, unicodeEscape)); + +export function stringEscape(s: string): string { + // "$this" is a template string in Kotlin so we have to escape $ + return _stringEscape(s).replace(/\$/g, "\\$"); +} diff --git a/packages/quicktype-core/src/language/Objective-C.ts b/packages/quicktype-core/src/language/Objective-C/ObjectiveCRenderer.ts similarity index 84% rename from packages/quicktype-core/src/language/Objective-C.ts rename to packages/quicktype-core/src/language/Objective-C/ObjectiveCRenderer.ts index 3b46b914b..faca214c8 100644 --- a/packages/quicktype-core/src/language/Objective-C.ts +++ b/packages/quicktype-core/src/language/Objective-C/ObjectiveCRenderer.ts @@ -1,231 +1,28 @@ import { iterableFirst, iterableSome, mapContains, mapFirst, mapSome } from "collection-utils"; -import unicode from "unicode-properties"; -import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../ConvenienceRenderer"; -import { type Name, Namer, funPrefixNamer } from "../Naming"; -import { type RenderContext } from "../Renderer"; +import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../../ConvenienceRenderer"; +import { type Name, Namer, funPrefixNamer } from "../../Naming"; +import { type RenderContext } from "../../Renderer"; +import { type OptionValues } from "../../RendererOptions"; +import { type Sourcelike, modifySource } from "../../Source"; +import { camelCase, fastIsUpperCase, repeatString, stringEscape } from "../../support/Strings"; +import { assert, defined } from "../../support/Support"; +import { type TargetLanguage } from "../../TargetLanguage"; +import { ArrayType, type ClassProperty, ClassType, EnumType, MapType, Type, UnionType } from "../../Type"; +import { isAnyOrNull, matchType, nullableFromUnion } from "../../TypeUtils"; + +import { forbiddenPropertyNames, keywords } from "./constants"; +import { type MemoryAttribute, type objectiveCOptions } from "./language"; import { - BooleanOption, - EnumOption, - type Option, - type OptionValues, - StringOption, - getOptionValues -} from "../RendererOptions"; -import { type Sourcelike, modifySource } from "../Source"; -import { - addPrefixIfNecessary, - allLowerWordStyle, - allUpperWordStyle, - camelCase, - combineWords, - fastIsUpperCase, - firstUpperWordStyle, - repeatString, - splitIntoWords, - stringEscape, - utf16LegalizeCharacters -} from "../support/Strings"; -import { assert, defined } from "../support/Support"; -import { TargetLanguage } from "../TargetLanguage"; -import { ArrayType, type ClassProperty, ClassType, EnumType, MapType, Type, UnionType } from "../Type"; -import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../types"; -import { isAnyOrNull, matchType, nullableFromUnion } from "../TypeUtils"; - -export type MemoryAttribute = "assign" | "strong" | "copy"; -export interface OutputFeatures { - implementation: boolean; - interface: boolean; -} + DEFAULT_CLASS_PREFIX, + forbiddenForEnumCases, + propertyNameStyle, + splitExtension, + staticEnumValuesIdentifier, + typeNameStyle +} from "./utils"; const DEBUG = false; -const DEFAULT_CLASS_PREFIX = "QT"; - -export const objcOptions = { - features: new EnumOption("features", "Interface and implementation", [ - ["all", { interface: true, implementation: true }], - ["interface", { interface: true, implementation: false }], - ["implementation", { interface: false, implementation: true }] - ]), - justTypes: new BooleanOption("just-types", "Plain types only", false), - marshallingFunctions: new BooleanOption("functions", "C-style functions", false), - classPrefix: new StringOption("class-prefix", "Class prefix", "PREFIX", DEFAULT_CLASS_PREFIX), - extraComments: new BooleanOption("extra-comments", "Extra comments", false) -}; - -export class ObjectiveCTargetLanguage extends TargetLanguage { - public constructor() { - super("Objective-C", ["objc", "objective-c", "objectivec"], "m"); - } - - protected getOptions(): Array> { - return [ - objcOptions.justTypes, - objcOptions.classPrefix, - objcOptions.features, - objcOptions.extraComments, - objcOptions.marshallingFunctions - ]; - } - - protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): ObjectiveCRenderer { - return new ObjectiveCRenderer(this, renderContext, getOptionValues(objcOptions, untypedOptionValues)); - } -} - -function typeNameStyle(prefix: string, original: string): string { - const words = splitIntoWords(original); - const result = combineWords( - words, - legalizeName, - firstUpperWordStyle, - firstUpperWordStyle, - allUpperWordStyle, - allUpperWordStyle, - "", - isStartCharacter - ); - return addPrefixIfNecessary(prefix, result); -} - -function propertyNameStyle(original: string, isBool = false): string { - // Objective-C developers are uncomfortable with property "id" - // so we use an alternate name in this special case. - if (original === "id") { - original = "identifier"; - } - - let words = splitIntoWords(original); - - if (isBool) { - if (words.length === 0) { - words = [{ word: "flag", isAcronym: false }]; - } else if (!words[0].isAcronym && !booleanPrefixes.includes(words[0].word)) { - words = [{ word: "is", isAcronym: false }, ...words]; - } - } - - // Properties cannot even begin with any of the forbidden names - // For example, properies named new* are treated differently by ARC - if (words.length > 0 && forbiddenPropertyNames.includes(words[0].word)) { - words = [{ word: "the", isAcronym: false }, ...words]; - } - - return combineWords( - words, - legalizeName, - allLowerWordStyle, - firstUpperWordStyle, - allLowerWordStyle, - allUpperWordStyle, - "", - isStartCharacter - ); -} - -const keywords = [ - /* - "_Bool", - "_Complex", - "_Imaginary", - */ - "asm", - "atomic", - "auto", - "bool", - "break", - "case", - "char", - "const", - "continue", - "default", - "do", - "double", - "else", - "enum", - "extern", - "false", - "float", - "for", - "goto", - "if", - "inline", - "int", - "long", - "nil", - "nonatomic", - "register", - "restrict", - "retain", - "return", - "short", - "signed", - "sizeof", - "static", - "struct", - "switch", - "typedef", - "typeof", - "true", - "union", - "unsigned", - "void", - "volatile", - "while" -]; - -const forbiddenPropertyNames = [ - "id", - "hash", - "description", - "init", - "copy", - "mutableCopy", - "superclass", - "debugDescription", - "new" -]; - -const booleanPrefixes = [ - "is", - "are", - "were", - "was", - "will", - "all", - "some", - "many", - "has", - "have", - "had", - "does", - "do", - "requires", - "require", - "needs", - "need" -]; - -function isStartCharacter(utf16Unit: number): boolean { - return unicode.isAlphabetic(utf16Unit) || utf16Unit === 0x5f; // underscore -} - -function isPartCharacter(utf16Unit: number): boolean { - const category: string = unicode.getCategory(utf16Unit); - return ["Nd", "Pc", "Mn", "Mc"].includes(category) || isStartCharacter(utf16Unit); -} - -const legalizeName = utf16LegalizeCharacters(isPartCharacter); - -const staticEnumValuesIdentifier = "values"; -const forbiddenForEnumCases = ["new", staticEnumValuesIdentifier]; - -function splitExtension(filename: string): [string, string] { - const i = filename.lastIndexOf("."); - const extension = i !== -1 ? filename.split(".").pop() : "m"; - filename = i !== -1 ? filename.slice(0, i) : filename; - return [filename, extension ?? "m"]; -} export class ObjectiveCRenderer extends ConvenienceRenderer { private _currentFilename: string | undefined; @@ -235,7 +32,7 @@ export class ObjectiveCRenderer extends ConvenienceRenderer { public constructor( targetLanguage: TargetLanguage, renderContext: RenderContext, - private readonly _options: OptionValues + private readonly _options: OptionValues ) { super(targetLanguage, renderContext); @@ -259,12 +56,12 @@ export class ObjectiveCRenderer extends ConvenienceRenderer { return name.slice(0, firstNonUpper - 1); } - protected forbiddenNamesForGlobalNamespace(): string[] { + protected forbiddenNamesForGlobalNamespace(): readonly string[] { return keywords; } protected forbiddenForObjectProperties(_c: ClassType, _className: Name): ForbiddenWordsInfo { - return { names: forbiddenPropertyNames, includeGlobalForbidden: true }; + return { names: forbiddenPropertyNames as unknown as string[], includeGlobalForbidden: true }; } protected forbiddenForEnumCases(_e: EnumType, _enumName: Name): ForbiddenWordsInfo { @@ -1087,15 +884,15 @@ export class ObjectiveCRenderer extends ConvenienceRenderer { protected emitMapFunction(): void { if (this.needsMap) { this.emitMultiline(`static id map(id collection, id (^f)(id value)) { - id result = nil; - if ([collection isKindOfClass:NSArray.class]) { - result = [NSMutableArray arrayWithCapacity:[collection count]]; - for (id x in collection) [result addObject:f(x)]; - } else if ([collection isKindOfClass:NSDictionary.class]) { - result = [NSMutableDictionary dictionaryWithCapacity:[collection count]]; - for (id key in collection) [result setObject:f([collection objectForKey:key]) forKey:key]; - } - return result; + id result = nil; + if ([collection isKindOfClass:NSArray.class]) { + result = [NSMutableArray arrayWithCapacity:[collection count]]; + for (id x in collection) [result addObject:f(x)]; + } else if ([collection isKindOfClass:NSDictionary.class]) { + result = [NSMutableDictionary dictionaryWithCapacity:[collection count]]; + for (id key in collection) [result setObject:f([collection objectForKey:key]) forKey:key]; + } + return result; }`); } } diff --git a/packages/quicktype-core/src/language/Objective-C/constants.ts b/packages/quicktype-core/src/language/Objective-C/constants.ts new file mode 100644 index 000000000..84ec8f95f --- /dev/null +++ b/packages/quicktype-core/src/language/Objective-C/constants.ts @@ -0,0 +1,82 @@ +export const keywords = [ + /* + "_Bool", + "_Complex", + "_Imaginary", + */ + "asm", + "atomic", + "auto", + "bool", + "break", + "case", + "char", + "const", + "continue", + "default", + "do", + "double", + "else", + "enum", + "extern", + "false", + "float", + "for", + "goto", + "if", + "inline", + "int", + "long", + "nil", + "nonatomic", + "register", + "restrict", + "retain", + "return", + "short", + "signed", + "sizeof", + "static", + "struct", + "switch", + "typedef", + "typeof", + "true", + "union", + "unsigned", + "void", + "volatile", + "while" +] as const; + +export const forbiddenPropertyNames = [ + "id", + "hash", + "description", + "init", + "copy", + "mutableCopy", + "superclass", + "debugDescription", + "new" +] as const; + +export const booleanPrefixes = [ + "is", + "are", + "were", + "was", + "will", + "all", + "some", + "many", + "has", + "have", + "had", + "does", + "do", + "requires", + "require", + "needs", + "need" +] as const; diff --git a/packages/quicktype-core/src/language/Objective-C/index.ts b/packages/quicktype-core/src/language/Objective-C/index.ts new file mode 100644 index 000000000..46926c8c6 --- /dev/null +++ b/packages/quicktype-core/src/language/Objective-C/index.ts @@ -0,0 +1,2 @@ +export { ObjectiveCTargetLanguage, objectiveCOptions } from "./language"; +export { ObjectiveCRenderer } from "./ObjectiveCRenderer"; diff --git a/packages/quicktype-core/src/language/Objective-C/language.ts b/packages/quicktype-core/src/language/Objective-C/language.ts new file mode 100644 index 000000000..cb34271f9 --- /dev/null +++ b/packages/quicktype-core/src/language/Objective-C/language.ts @@ -0,0 +1,45 @@ +import { type RenderContext } from "../../Renderer"; +import { BooleanOption, EnumOption, type Option, StringOption, getOptionValues } from "../../RendererOptions"; +import { TargetLanguage } from "../../TargetLanguage"; +import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../../types"; + +import { ObjectiveCRenderer } from "./ObjectiveCRenderer"; +import { DEFAULT_CLASS_PREFIX } from "./utils"; + +export type MemoryAttribute = "assign" | "strong" | "copy"; +export interface OutputFeatures { + implementation: boolean; + interface: boolean; +} + +export const objectiveCOptions = { + features: new EnumOption("features", "Interface and implementation", [ + ["all", { interface: true, implementation: true }], + ["interface", { interface: true, implementation: false }], + ["implementation", { interface: false, implementation: true }] + ]), + justTypes: new BooleanOption("just-types", "Plain types only", false), + marshallingFunctions: new BooleanOption("functions", "C-style functions", false), + classPrefix: new StringOption("class-prefix", "Class prefix", "PREFIX", DEFAULT_CLASS_PREFIX), + extraComments: new BooleanOption("extra-comments", "Extra comments", false) +}; + +export class ObjectiveCTargetLanguage extends TargetLanguage { + public constructor() { + super("Objective-C", ["objc", "objective-c", "objectivec"], "m"); + } + + protected getOptions(): Array> { + return [ + objectiveCOptions.justTypes, + objectiveCOptions.classPrefix, + objectiveCOptions.features, + objectiveCOptions.extraComments, + objectiveCOptions.marshallingFunctions + ]; + } + + protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): ObjectiveCRenderer { + return new ObjectiveCRenderer(this, renderContext, getOptionValues(objectiveCOptions, untypedOptionValues)); + } +} diff --git a/packages/quicktype-core/src/language/Objective-C/utils.ts b/packages/quicktype-core/src/language/Objective-C/utils.ts new file mode 100644 index 000000000..cdff7db57 --- /dev/null +++ b/packages/quicktype-core/src/language/Objective-C/utils.ts @@ -0,0 +1,88 @@ +import unicode from "unicode-properties"; + +import { + addPrefixIfNecessary, + allLowerWordStyle, + allUpperWordStyle, + combineWords, + firstUpperWordStyle, + splitIntoWords, + utf16LegalizeCharacters +} from "../../support/Strings"; + +import { booleanPrefixes, forbiddenPropertyNames } from "./constants"; + +export const DEFAULT_CLASS_PREFIX = "QT"; + +export function typeNameStyle(prefix: string, original: string): string { + const words = splitIntoWords(original); + const result = combineWords( + words, + legalizeName, + firstUpperWordStyle, + firstUpperWordStyle, + allUpperWordStyle, + allUpperWordStyle, + "", + isStartCharacter + ); + return addPrefixIfNecessary(prefix, result); +} + +export function propertyNameStyle(original: string, isBool = false): string { + // Objective-C developers are uncomfortable with property "id" + // so we use an alternate name in this special case. + if (original === "id") { + original = "identifier"; + } + + let words = splitIntoWords(original); + + if (isBool) { + if (words.length === 0) { + words = [{ word: "flag", isAcronym: false }]; + // @ts-expect-error needs strict type + } else if (!words[0].isAcronym && !booleanPrefixes.includes(words[0].word)) { + words = [{ word: "is", isAcronym: false }, ...words]; + } + } + + // Properties cannot even begin with any of the forbidden names + // For example, properies named new* are treated differently by ARC + // @ts-expect-error needs strict type + if (words.length > 0 && forbiddenPropertyNames.includes(words[0].word)) { + words = [{ word: "the", isAcronym: false }, ...words]; + } + + return combineWords( + words, + legalizeName, + allLowerWordStyle, + firstUpperWordStyle, + allLowerWordStyle, + allUpperWordStyle, + "", + isStartCharacter + ); +} + +function isStartCharacter(utf16Unit: number): boolean { + return unicode.isAlphabetic(utf16Unit) || utf16Unit === 0x5f; // underscore +} + +function isPartCharacter(utf16Unit: number): boolean { + const category: string = unicode.getCategory(utf16Unit); + return ["Nd", "Pc", "Mn", "Mc"].includes(category) || isStartCharacter(utf16Unit); +} + +const legalizeName = utf16LegalizeCharacters(isPartCharacter); + +export const staticEnumValuesIdentifier = "values"; +export const forbiddenForEnumCases = ["new", staticEnumValuesIdentifier]; + +export function splitExtension(filename: string): [string, string] { + const i = filename.lastIndexOf("."); + const extension = i !== -1 ? filename.split(".").pop() : "m"; + filename = i !== -1 ? filename.slice(0, i) : filename; + return [filename, extension ?? "m"]; +} diff --git a/packages/quicktype-core/src/language/Php.ts b/packages/quicktype-core/src/language/Php/PhpRenderer.ts similarity index 91% rename from packages/quicktype-core/src/language/Php.ts rename to packages/quicktype-core/src/language/Php/PhpRenderer.ts index e448bb1ee..720f5b6e4 100644 --- a/packages/quicktype-core/src/language/Php.ts +++ b/packages/quicktype-core/src/language/Php/PhpRenderer.ts @@ -1,100 +1,19 @@ import * as _ from "lodash"; -import { type PrimitiveStringTypeKind, type StringTypeMapping, type TransformedStringTypeKind } from ".."; -import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../Annotation"; -import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../ConvenienceRenderer"; -import { DependencyName, type Name, type Namer, funPrefixNamer } from "../Naming"; -import { type RenderContext } from "../Renderer"; -import { BooleanOption, type Option, type OptionValues, getOptionValues } from "../RendererOptions"; -import { type Sourcelike, maybeAnnotated } from "../Source"; -import { AcronymStyleOptions, acronymOption, acronymStyle } from "../support/Acronyms"; -import { - allLowerWordStyle, - allUpperWordStyle, - combineWords, - escapeNonPrintableMapper, - firstUpperWordStyle, - isAscii, - isDigit, - isLetter, - splitIntoWords, - standardUnicodeHexEscape, - utf16ConcatMap, - utf16LegalizeCharacters -} from "../support/Strings"; -import { defined } from "../support/Support"; -import { TargetLanguage } from "../TargetLanguage"; -import { type ClassProperty, type ClassType, type EnumType, type Type, type UnionType } from "../Type"; -import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../types"; -import { directlyReachableSingleNamedType, matchType, nullableFromUnion } from "../TypeUtils"; - -export const phpOptions = { - withGet: new BooleanOption("with-get", "Create Getter", true), - fastGet: new BooleanOption("fast-get", "getter without validation", false), - withSet: new BooleanOption("with-set", "Create Setter", false), - withClosing: new BooleanOption("with-closing", "PHP Closing Tag", false), - acronymStyle: acronymOption(AcronymStyleOptions.Pascal) -}; - -export class PhpTargetLanguage extends TargetLanguage { - public constructor() { - super("PHP", ["php"], "php"); - } - - protected getOptions(): Array> { - return _.values(phpOptions); - } - - public get supportsUnionsWithBothNumberTypes(): boolean { - return true; - } - - protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): PhpRenderer { - const options = getOptionValues(phpOptions, untypedOptionValues); - return new PhpRenderer(this, renderContext, options); - } - - public get stringTypeMapping(): StringTypeMapping { - const mapping: Map = new Map(); - mapping.set("date", "date"); // TODO is not implemented yet - mapping.set("time", "time"); // TODO is not implemented yet - mapping.set("uuid", "uuid"); // TODO is not implemented yet - mapping.set("date-time", "date-time"); - return mapping; - } -} - -export const stringEscape = utf16ConcatMap(escapeNonPrintableMapper(isAscii, standardUnicodeHexEscape)); - -function isStartCharacter(codePoint: number): boolean { - if (codePoint === 0x5f) return true; // underscore - return isAscii(codePoint) && isLetter(codePoint); -} - -function isPartCharacter(codePoint: number): boolean { - return isStartCharacter(codePoint) || (isAscii(codePoint) && isDigit(codePoint)); -} - -const legalizeName = utf16LegalizeCharacters(isPartCharacter); - -export function phpNameStyle( - startWithUpper: boolean, - upperUnderscore: boolean, - original: string, - acronymsStyle: (s: string) => string = allUpperWordStyle -): string { - const words = splitIntoWords(original); - return combineWords( - words, - legalizeName, - upperUnderscore ? allUpperWordStyle : startWithUpper ? firstUpperWordStyle : allLowerWordStyle, - upperUnderscore ? allUpperWordStyle : firstUpperWordStyle, - upperUnderscore || startWithUpper ? allUpperWordStyle : allLowerWordStyle, - acronymsStyle, - upperUnderscore ? "_" : "", - isStartCharacter - ); -} +import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../../Annotation"; +import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../../ConvenienceRenderer"; +import { DependencyName, type Name, type Namer, funPrefixNamer } from "../../Naming"; +import { type RenderContext } from "../../Renderer"; +import { type OptionValues } from "../../RendererOptions"; +import { type Sourcelike, maybeAnnotated } from "../../Source"; +import { acronymStyle } from "../../support/Acronyms"; +import { defined } from "../../support/Support"; +import { type TargetLanguage } from "../../TargetLanguage"; +import { type ClassProperty, type ClassType, type EnumType, type Type, type UnionType } from "../../Type"; +import { directlyReachableSingleNamedType, matchType, nullableFromUnion } from "../../TypeUtils"; + +import { type phpOptions } from "./language"; +import { phpNameStyle, stringEscape } from "./utils"; export interface FunctionNames { readonly from: Name; diff --git a/packages/quicktype-core/src/language/Php/index.ts b/packages/quicktype-core/src/language/Php/index.ts new file mode 100644 index 000000000..2e11f2650 --- /dev/null +++ b/packages/quicktype-core/src/language/Php/index.ts @@ -0,0 +1,2 @@ +export { PhpTargetLanguage, phpOptions } from "./language"; +export { PhpRenderer } from "./PhpRenderer"; diff --git a/packages/quicktype-core/src/language/Php/language.ts b/packages/quicktype-core/src/language/Php/language.ts new file mode 100644 index 000000000..cfcfe9654 --- /dev/null +++ b/packages/quicktype-core/src/language/Php/language.ts @@ -0,0 +1,46 @@ +import * as _ from "lodash"; + +import { type RenderContext } from "../../Renderer"; +import { BooleanOption, type Option, getOptionValues } from "../../RendererOptions"; +import { AcronymStyleOptions, acronymOption } from "../../support/Acronyms"; +import { TargetLanguage } from "../../TargetLanguage"; +import { type PrimitiveStringTypeKind, type TransformedStringTypeKind } from "../../Type"; +import { type StringTypeMapping } from "../../TypeBuilder"; +import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../../types"; + +import { PhpRenderer } from "./PhpRenderer"; + +export const phpOptions = { + withGet: new BooleanOption("with-get", "Create Getter", true), + fastGet: new BooleanOption("fast-get", "getter without validation", false), + withSet: new BooleanOption("with-set", "Create Setter", false), + withClosing: new BooleanOption("with-closing", "PHP Closing Tag", false), + acronymStyle: acronymOption(AcronymStyleOptions.Pascal) +}; +export class PhpTargetLanguage extends TargetLanguage { + public constructor() { + super("PHP", ["php"], "php"); + } + + protected getOptions(): Array> { + return _.values(phpOptions); + } + + public get supportsUnionsWithBothNumberTypes(): boolean { + return true; + } + + protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): PhpRenderer { + const options = getOptionValues(phpOptions, untypedOptionValues); + return new PhpRenderer(this, renderContext, options); + } + + public get stringTypeMapping(): StringTypeMapping { + const mapping: Map = new Map(); + mapping.set("date", "date"); // TODO is not implemented yet + mapping.set("time", "time"); // TODO is not implemented yet + mapping.set("uuid", "uuid"); // TODO is not implemented yet + mapping.set("date-time", "date-time"); + return mapping; + } +} diff --git a/packages/quicktype-core/src/language/Php/utils.ts b/packages/quicktype-core/src/language/Php/utils.ts new file mode 100644 index 000000000..2ccb0e228 --- /dev/null +++ b/packages/quicktype-core/src/language/Php/utils.ts @@ -0,0 +1,48 @@ +import * as _ from "lodash"; + +import { + allLowerWordStyle, + allUpperWordStyle, + combineWords, + escapeNonPrintableMapper, + firstUpperWordStyle, + isAscii, + isDigit, + isLetter, + splitIntoWords, + standardUnicodeHexEscape, + utf16ConcatMap, + utf16LegalizeCharacters +} from "../../support/Strings"; + +export const stringEscape = utf16ConcatMap(escapeNonPrintableMapper(isAscii, standardUnicodeHexEscape)); + +function isStartCharacter(codePoint: number): boolean { + if (codePoint === 0x5f) return true; // underscore + return isAscii(codePoint) && isLetter(codePoint); +} + +function isPartCharacter(codePoint: number): boolean { + return isStartCharacter(codePoint) || (isAscii(codePoint) && isDigit(codePoint)); +} + +const legalizeName = utf16LegalizeCharacters(isPartCharacter); + +export function phpNameStyle( + startWithUpper: boolean, + upperUnderscore: boolean, + original: string, + acronymsStyle: (s: string) => string = allUpperWordStyle +): string { + const words = splitIntoWords(original); + return combineWords( + words, + legalizeName, + upperUnderscore ? allUpperWordStyle : startWithUpper ? firstUpperWordStyle : allLowerWordStyle, + upperUnderscore ? allUpperWordStyle : firstUpperWordStyle, + upperUnderscore || startWithUpper ? allUpperWordStyle : allLowerWordStyle, + acronymsStyle, + upperUnderscore ? "_" : "", + isStartCharacter + ); +} diff --git a/packages/quicktype-core/src/language/Pike.ts b/packages/quicktype-core/src/language/Pike/PikeRenderer.ts similarity index 81% rename from packages/quicktype-core/src/language/Pike.ts rename to packages/quicktype-core/src/language/Pike/PikeRenderer.ts index ea7f9d3a5..01e383c6e 100644 --- a/packages/quicktype-core/src/language/Pike.ts +++ b/packages/quicktype-core/src/language/Pike/PikeRenderer.ts @@ -1,89 +1,20 @@ -import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../ConvenienceRenderer"; -import { type Name, type Namer, funPrefixNamer } from "../Naming"; -import { type RenderContext } from "../Renderer"; -import { type Option } from "../RendererOptions"; -import { type MultiWord, type Sourcelike, multiWord, parenIfNeeded, singleWord } from "../Source"; -import { isLetterOrUnderscoreOrDigit, legalizeCharacters, makeNameStyle, stringEscape } from "../support/Strings"; -import { TargetLanguage } from "../TargetLanguage"; -import { ArrayType, type ClassType, type EnumType, MapType, PrimitiveType, type Type, type UnionType } from "../Type"; -import { type FixMeOptionsAnyType } from "../types"; -import { matchType, nullableFromUnion, removeNullFromUnion } from "../TypeUtils"; - -export const pikeOptions = {}; - -const keywords = [ - "auto", - "nomask", - "final", - "static", - "extern", - "private", - "local", - "public", - "protected", - "inline", - "optional", - "variant", - "void", - "mixed", - "array", - "__attribute__", - "__deprecated__", - "mapping", - "multiset", - "object", - "function", - "__func__", - "program", - "string", - "float", - "int", - "enum", - "typedef", - "if", - "do", - "for", - "while", - "else", - "foreach", - "catch", - "gauge", - "class", - "break", - "case", - "const", - "constant", - "continue", - "default", - "import", - "inherit", - "lambda", - "predef", - "return", - "sscanf", - "switch", - "typeof", - "global" -]; - -const legalizeName = legalizeCharacters(isLetterOrUnderscoreOrDigit); -const enumNamingFunction = funPrefixNamer("enumNamer", makeNameStyle("upper-underscore", legalizeName)); -const namingFunction = funPrefixNamer("genericNamer", makeNameStyle("underscore", legalizeName)); -const namedTypeNamingFunction = funPrefixNamer("typeNamer", makeNameStyle("pascal", legalizeName)); - -export class PikeTargetLanguage extends TargetLanguage { - public constructor() { - super("Pike", ["pike", "pikelang"], "pmod"); - } - - protected getOptions(): Array> { - return []; - } - - protected makeRenderer(renderContext: RenderContext): PikeRenderer { - return new PikeRenderer(this, renderContext); - } -} +import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../../ConvenienceRenderer"; +import { type Name, type Namer } from "../../Naming"; +import { type MultiWord, type Sourcelike, multiWord, parenIfNeeded, singleWord } from "../../Source"; +import { stringEscape } from "../../support/Strings"; +import { + ArrayType, + type ClassType, + type EnumType, + MapType, + PrimitiveType, + type Type, + type UnionType +} from "../../Type"; +import { matchType, nullableFromUnion, removeNullFromUnion } from "../../TypeUtils"; + +import { keywords } from "./constants"; +import { enumNamingFunction, namedTypeNamingFunction, namingFunction } from "./utils"; export class PikeRenderer extends ConvenienceRenderer { protected emitSourceStructure(): void { diff --git a/packages/quicktype-core/src/language/Pike/constants.ts b/packages/quicktype-core/src/language/Pike/constants.ts new file mode 100644 index 000000000..d3927948f --- /dev/null +++ b/packages/quicktype-core/src/language/Pike/constants.ts @@ -0,0 +1,54 @@ +export const keywords = [ + "auto", + "nomask", + "final", + "static", + "extern", + "private", + "local", + "public", + "protected", + "inline", + "optional", + "variant", + "void", + "mixed", + "array", + "__attribute__", + "__deprecated__", + "mapping", + "multiset", + "object", + "function", + "__func__", + "program", + "string", + "float", + "int", + "enum", + "typedef", + "if", + "do", + "for", + "while", + "else", + "foreach", + "catch", + "gauge", + "class", + "break", + "case", + "const", + "constant", + "continue", + "default", + "import", + "inherit", + "lambda", + "predef", + "return", + "sscanf", + "switch", + "typeof", + "global" +] as const; diff --git a/packages/quicktype-core/src/language/Pike/index.ts b/packages/quicktype-core/src/language/Pike/index.ts new file mode 100644 index 000000000..2b80f3674 --- /dev/null +++ b/packages/quicktype-core/src/language/Pike/index.ts @@ -0,0 +1,2 @@ +export { PikeTargetLanguage, pikeOptions } from "./language"; +export { PikeRenderer } from "./PikeRenderer"; diff --git a/packages/quicktype-core/src/language/Pike/language.ts b/packages/quicktype-core/src/language/Pike/language.ts new file mode 100644 index 000000000..1772085a0 --- /dev/null +++ b/packages/quicktype-core/src/language/Pike/language.ts @@ -0,0 +1,22 @@ +import { type RenderContext } from "../../Renderer"; +import { type Option } from "../../RendererOptions"; +import { TargetLanguage } from "../../TargetLanguage"; +import { type FixMeOptionsAnyType } from "../../types"; + +import { PikeRenderer } from "./PikeRenderer"; + +export const pikeOptions = {}; + +export class PikeTargetLanguage extends TargetLanguage { + public constructor() { + super("Pike", ["pike", "pikelang"], "pmod"); + } + + protected getOptions(): Array> { + return []; + } + + protected makeRenderer(renderContext: RenderContext): PikeRenderer { + return new PikeRenderer(this, renderContext); + } +} diff --git a/packages/quicktype-core/src/language/Pike/utils.ts b/packages/quicktype-core/src/language/Pike/utils.ts new file mode 100644 index 000000000..f097ad2c6 --- /dev/null +++ b/packages/quicktype-core/src/language/Pike/utils.ts @@ -0,0 +1,7 @@ +import { funPrefixNamer } from "../../Naming"; +import { isLetterOrUnderscoreOrDigit, legalizeCharacters, makeNameStyle } from "../../support/Strings"; + +const legalizeName = legalizeCharacters(isLetterOrUnderscoreOrDigit); +export const enumNamingFunction = funPrefixNamer("enumNamer", makeNameStyle("upper-underscore", legalizeName)); +export const namingFunction = funPrefixNamer("genericNamer", makeNameStyle("underscore", legalizeName)); +export const namedTypeNamingFunction = funPrefixNamer("typeNamer", makeNameStyle("pascal", legalizeName)); diff --git a/packages/quicktype-core/src/language/Python.ts b/packages/quicktype-core/src/language/Python/JSONPythonRenderer.ts similarity index 63% rename from packages/quicktype-core/src/language/Python.ts rename to packages/quicktype-core/src/language/Python/JSONPythonRenderer.ts index dc3f5b9e9..28d16eece 100644 --- a/packages/quicktype-core/src/language/Python.ts +++ b/packages/quicktype-core/src/language/Python/JSONPythonRenderer.ts @@ -1,31 +1,9 @@ -import { - arrayIntercalate, - iterableFirst, - iterableSome, - mapSortBy, - mapUpdateInto, - setUnionInto -} from "collection-utils"; -import unicode from "unicode-properties"; - -import { ConvenienceRenderer, type ForbiddenWordsInfo, topLevelNameOrder } from "../ConvenienceRenderer"; -import { DependencyName, type Name, type Namer, funPrefixNamer } from "../Naming"; -import { type RenderContext } from "../Renderer"; -import { BooleanOption, EnumOption, type Option, type OptionValues, getOptionValues } from "../RendererOptions"; -import { type MultiWord, type Sourcelike, modifySource, multiWord, parenIfNeeded, singleWord } from "../Source"; -import { - type WordStyle, - allLowerWordStyle, - allUpperWordStyle, - combineWords, - firstUpperWordStyle, - originalWord, - splitIntoWords, - stringEscape, - utf16LegalizeCharacters -} from "../support/Strings"; -import { assertNever, defined, panic } from "../support/Support"; -import { TargetLanguage } from "../TargetLanguage"; +import { arrayIntercalate } from "collection-utils"; + +import { topLevelNameOrder } from "../../ConvenienceRenderer"; +import { DependencyName, type Name, funPrefixNamer } from "../../Naming"; +import { type MultiWord, type Sourcelike, multiWord, parenIfNeeded, singleWord } from "../../Source"; +import { assertNever, defined, panic } from "../../support/Support"; import { ChoiceTransformer, DecodingChoiceTransformer, @@ -36,512 +14,13 @@ import { type Transformer, UnionInstantiationTransformer, UnionMemberMatchTransformer, - followTargetType, transformationForType -} from "../Transformers"; -import { - type ClassProperty, - ClassType, - EnumType, - type PrimitiveStringTypeKind, - type TransformedStringTypeKind, - type Type, - UnionType -} from "../Type"; -import { type StringTypeMapping } from "../TypeBuilder"; -import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../types"; -import { matchType, nullableFromUnion, removeNullFromUnion } from "../TypeUtils"; - -const forbiddenTypeNames = [ - "Any", - "True", - "False", - "None", - "Enum", - "List", - "Dict", - "Optional", - "Union", - "Iterable", - "Type", - "TypeVar", - "T", - "EnumT" -]; -const forbiddenPropertyNames = [ - "and", - "as", - "assert", - "async", - "await", - "bool", - "break", - "class", - "continue", - "datetime", - "def", - "del", - "dict", - "elif", - "else", - "except", - "finally", - "float", - "for", - "from", - "global", - "if", - "import", - "in", - "int", - "is", - "lambda", - "nonlocal", - "not", - "or", - "pass", - "print", - "raise", - "return", - "self", - "str", - "try", - "while", - "with", - "yield" -]; - -export interface PythonFeatures { - dataClasses: boolean; - typeHints: boolean; -} - -export const pythonOptions = { - features: new EnumOption( - "python-version", - "Python version", - [ - ["3.5", { typeHints: false, dataClasses: false }], - ["3.6", { typeHints: true, dataClasses: false }], - ["3.7", { typeHints: true, dataClasses: true }] - ], - "3.6" - ), - justTypes: new BooleanOption("just-types", "Classes only", false), - nicePropertyNames: new BooleanOption("nice-property-names", "Transform property names to be Pythonic", true) -}; - -export class PythonTargetLanguage extends TargetLanguage { - protected getOptions(): Array> { - return [pythonOptions.features, pythonOptions.justTypes, pythonOptions.nicePropertyNames]; - } - - public get stringTypeMapping(): StringTypeMapping { - const mapping: Map = new Map(); - const dateTimeType = "date-time"; - mapping.set("date", dateTimeType); - mapping.set("time", dateTimeType); - mapping.set("date-time", dateTimeType); - mapping.set("uuid", "uuid"); - mapping.set("integer-string", "integer-string"); - mapping.set("bool-string", "bool-string"); - return mapping; - } - - public get supportsUnionsWithBothNumberTypes(): boolean { - return true; - } - - public get supportsOptionalClassProperties(): boolean { - return false; - } - - public needsTransformerForType(t: Type): boolean { - if (t instanceof UnionType) { - return iterableSome(t.members, m => this.needsTransformerForType(m)); - } - - return t.kind === "integer-string" || t.kind === "bool-string"; - } - - protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): PythonRenderer { - const options = getOptionValues(pythonOptions, untypedOptionValues); - if (options.justTypes) { - return new PythonRenderer(this, renderContext, options); - } else { - return new JSONPythonRenderer(this, renderContext, options); - } - } -} - -function isNormalizedStartCharacter3(utf16Unit: number): boolean { - // FIXME: add Other_ID_Start - https://docs.python.org/3/reference/lexical_analysis.html#identifiers - const category: string = unicode.getCategory(utf16Unit); - return ["Lu", "Ll", "Lt", "Lm", "Lo", "Nl"].includes(category); -} - -function isNormalizedPartCharacter3(utf16Unit: number): boolean { - // FIXME: add Other_ID_Continue - https://docs.python.org/3/reference/lexical_analysis.html#identifiers - if (isNormalizedStartCharacter3(utf16Unit)) return true; - const category: string = unicode.getCategory(utf16Unit); - return ["Mn", "Mc", "Nd", "Pc"].includes(category); -} - -function isStartCharacter3(utf16Unit: number): boolean { - const s = String.fromCharCode(utf16Unit).normalize("NFKC"); - const l = s.length; - if (l === 0 || !isNormalizedStartCharacter3(s.charCodeAt(0))) return false; - for (let i = 1; i < l; i++) { - if (!isNormalizedPartCharacter3(s.charCodeAt(i))) return false; - } - - return true; -} - -function isPartCharacter3(utf16Unit: number): boolean { - const s = String.fromCharCode(utf16Unit).normalize("NFKC"); - const l = s.length; - for (let i = 0; i < l; i++) { - if (!isNormalizedPartCharacter3(s.charCodeAt(i))) return false; - } - - return true; -} - -const legalizeName3 = utf16LegalizeCharacters(isPartCharacter3); - -function classNameStyle(original: string): string { - const words = splitIntoWords(original); - return combineWords( - words, - legalizeName3, - firstUpperWordStyle, - firstUpperWordStyle, - allUpperWordStyle, - allUpperWordStyle, - "", - isStartCharacter3 - ); -} - -function getWordStyle(uppercase: boolean, forceSnakeNameStyle: boolean): WordStyle { - if (!forceSnakeNameStyle) { - return originalWord; - } - - return uppercase ? allUpperWordStyle : allLowerWordStyle; -} - -function snakeNameStyle(original: string, uppercase: boolean, forceSnakeNameStyle: boolean): string { - const wordStyle = getWordStyle(uppercase, forceSnakeNameStyle); - const separator = forceSnakeNameStyle ? "_" : ""; - const words = splitIntoWords(original); - return combineWords(words, legalizeName3, wordStyle, wordStyle, wordStyle, wordStyle, separator, isStartCharacter3); -} - -export class PythonRenderer extends ConvenienceRenderer { - private readonly imports: Map> = new Map(); - - private readonly declaredTypes: Set = new Set(); - - public constructor( - targetLanguage: TargetLanguage, - renderContext: RenderContext, - protected readonly pyOptions: OptionValues - ) { - super(targetLanguage, renderContext); - } - - protected forbiddenNamesForGlobalNamespace(): string[] { - return forbiddenTypeNames; - } - - protected forbiddenForObjectProperties(_: ClassType, _classNamed: Name): ForbiddenWordsInfo { - return { names: forbiddenPropertyNames, includeGlobalForbidden: false }; - } - - protected makeNamedTypeNamer(): Namer { - return funPrefixNamer("type", classNameStyle); - } - - protected namerForObjectProperty(): Namer { - return funPrefixNamer("property", s => snakeNameStyle(s, false, this.pyOptions.nicePropertyNames)); - } - - protected makeUnionMemberNamer(): null { - return null; - } - - protected makeEnumCaseNamer(): Namer { - return funPrefixNamer("enum-case", s => snakeNameStyle(s, true, this.pyOptions.nicePropertyNames)); - } - - protected get commentLineStart(): string { - return "# "; - } - - protected emitDescriptionBlock(lines: Sourcelike[]): void { - if (lines.length === 1) { - const docstring = modifySource(content => { - if (content.endsWith('"')) { - return content.slice(0, -1) + '\\"'; - } - - return content; - }, lines[0]); - this.emitComments([{ customLines: [docstring], lineStart: '"""', lineEnd: '"""' }]); - } else { - this.emitCommentLines(lines, { - firstLineStart: '"""', - lineStart: "", - afterComment: '"""' - }); - } - } - - protected get needsTypeDeclarationBeforeUse(): boolean { - return true; - } - - protected canBeForwardDeclared(t: Type): boolean { - const kind = t.kind; - return kind === "class" || kind === "enum"; - } - - protected emitBlock(line: Sourcelike, f: () => void): void { - this.emitLine(line); - this.indent(f); - } - - protected string(s: string): Sourcelike { - const openQuote = '"'; - return [openQuote, stringEscape(s), '"']; - } - - protected withImport(module: string, name: string): Sourcelike { - if (this.pyOptions.features.typeHints || module !== "typing") { - // FIXME: This is ugly. We should rather not generate that import in the first - // place, but right now we just make the type source and then throw it away. It's - // not a performance issue, so it's fine, I just bemoan this special case, and - // potential others down the road. - mapUpdateInto(this.imports, module, s => (s ? setUnionInto(s, [name]) : new Set([name]))); - } - - return name; - } - - protected withTyping(name: string): Sourcelike { - return this.withImport("typing", name); - } - - protected namedType(t: Type): Sourcelike { - const name = this.nameForNamedType(t); - if (this.declaredTypes.has(t)) return name; - return ["'", name, "'"]; - } - - protected pythonType(t: Type, _isRootTypeDef = false): Sourcelike { - const actualType = followTargetType(t); - - return matchType( - actualType, - _anyType => this.withTyping("Any"), - _nullType => "None", - _boolType => "bool", - _integerType => "int", - _doubletype => "float", - _stringType => "str", - arrayType => [this.withTyping("List"), "[", this.pythonType(arrayType.items), "]"], - classType => this.namedType(classType), - mapType => [this.withTyping("Dict"), "[str, ", this.pythonType(mapType.values), "]"], - enumType => this.namedType(enumType), - unionType => { - const [hasNull, nonNulls] = removeNullFromUnion(unionType); - const memberTypes = Array.from(nonNulls).map(m => this.pythonType(m)); - - if (hasNull !== null) { - let rest: string[] = []; - if (!this.getAlphabetizeProperties() && this.pyOptions.features.dataClasses && _isRootTypeDef) { - // Only push "= None" if this is a root level type def - // otherwise we may get type defs like List[Optional[int] = None] - // which are invalid - rest.push(" = None"); - } - - if (nonNulls.size > 1) { - this.withImport("typing", "Union"); - return [ - this.withTyping("Optional"), - "[Union[", - arrayIntercalate(", ", memberTypes), - "]]", - ...rest - ]; - } else { - return [this.withTyping("Optional"), "[", defined(iterableFirst(memberTypes)), "]", ...rest]; - } - } else { - return [this.withTyping("Union"), "[", arrayIntercalate(", ", memberTypes), "]"]; - } - }, - transformedStringType => { - if (transformedStringType.kind === "date-time") { - return this.withImport("datetime", "datetime"); - } - - if (transformedStringType.kind === "uuid") { - return this.withImport("uuid", "UUID"); - } - - return panic(`Transformed type ${transformedStringType.kind} not supported`); - } - ); - } - - protected declarationLine(t: Type): Sourcelike { - if (t instanceof ClassType) { - return ["class ", this.nameForNamedType(t), ":"]; - } - - if (t instanceof EnumType) { - return ["class ", this.nameForNamedType(t), "(", this.withImport("enum", "Enum"), "):"]; - } - - return panic(`Can't declare type ${t.kind}`); - } +} from "../../Transformers"; +import { type ClassType, type Type } from "../../Type"; +import { matchType } from "../../TypeUtils"; - protected declareType(t: T, emitter: () => void): void { - this.emitBlock(this.declarationLine(t), () => { - this.emitDescription(this.descriptionForType(t)); - emitter(); - }); - this.declaredTypes.add(t); - } - - protected emitClassMembers(t: ClassType): void { - if (this.pyOptions.features.dataClasses) return; - - const args: Sourcelike[] = []; - this.forEachClassProperty(t, "none", (name, _, cp) => { - args.push([name, this.typeHint(": ", this.pythonType(cp.type))]); - }); - this.emitBlock( - ["def __init__(self, ", arrayIntercalate(", ", args), ")", this.typeHint(" -> None"), ":"], - () => { - if (args.length === 0) { - this.emitLine("pass"); - } else { - this.forEachClassProperty(t, "none", name => { - this.emitLine("self.", name, " = ", name); - }); - } - } - ); - } - - protected typeHint(...sl: Sourcelike[]): Sourcelike { - if (this.pyOptions.features.typeHints) { - return sl; - } - - return []; - } - - protected typingDecl(name: Sourcelike, type: string): Sourcelike { - return [name, this.typeHint(": ", this.withTyping(type))]; - } - - protected typingReturn(type: string): Sourcelike { - return this.typeHint(" -> ", this.withTyping(type)); - } - - protected sortClassProperties( - properties: ReadonlyMap, - propertyNames: ReadonlyMap - ): ReadonlyMap { - if (this.pyOptions.features.dataClasses) { - return mapSortBy(properties, (p: ClassProperty) => { - return (p.type instanceof UnionType && nullableFromUnion(p.type) != null) || p.isOptional ? 1 : 0; - }); - } else { - return super.sortClassProperties(properties, propertyNames); - } - } - - protected emitClass(t: ClassType): void { - if (this.pyOptions.features.dataClasses) { - this.emitLine("@", this.withImport("dataclasses", "dataclass")); - } - - this.declareType(t, () => { - if (this.pyOptions.features.typeHints) { - if (t.getProperties().size === 0) { - this.emitLine("pass"); - } else { - this.forEachClassProperty(t, "none", (name, jsonName, cp) => { - this.emitLine(name, this.typeHint(": ", this.pythonType(cp.type, true))); - this.emitDescription(this.descriptionForClassProperty(t, jsonName)); - }); - } - - this.ensureBlankLine(); - } - - this.emitClassMembers(t); - }); - } - - protected emitEnum(t: EnumType): void { - this.declareType(t, () => { - this.forEachEnumCase(t, "none", (name, jsonName) => { - this.emitLine([name, " = ", this.string(jsonName)]); - }); - }); - } - - protected emitImports(): void { - this.imports.forEach((names, module) => { - this.emitLine("from ", module, " import ", Array.from(names).join(", ")); - }); - } - - protected emitSupportCode(): void { - return; - } - - protected emitClosingCode(): void { - return; - } - - protected emitSourceStructure(_givenOutputFilename: string): void { - const declarationLines = this.gatherSource(() => { - this.forEachNamedType( - ["interposing", 2], - (c: ClassType) => this.emitClass(c), - e => this.emitEnum(e), - _u => { - return; - } - ); - }); - - const closingLines = this.gatherSource(() => this.emitClosingCode()); - const supportLines = this.gatherSource(() => this.emitSupportCode()); - - if (this.leadingComments !== undefined) { - this.emitComments(this.leadingComments); - } - - this.ensureBlankLine(); - this.emitImports(); - this.ensureBlankLine(2); - this.emitGatheredSource(supportLines); - this.ensureBlankLine(2); - this.emitGatheredSource(declarationLines); - this.ensureBlankLine(2); - this.emitGatheredSource(closingLines); - } -} +import { PythonRenderer } from "./PythonRenderer"; +import { snakeNameStyle } from "./utils"; export type ConverterFunction = | "none" diff --git a/packages/quicktype-core/src/language/Python/PythonRenderer.ts b/packages/quicktype-core/src/language/Python/PythonRenderer.ts new file mode 100644 index 000000000..63ec3c70d --- /dev/null +++ b/packages/quicktype-core/src/language/Python/PythonRenderer.ts @@ -0,0 +1,321 @@ +import { arrayIntercalate, iterableFirst, mapSortBy, mapUpdateInto, setUnionInto } from "collection-utils"; + +import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../../ConvenienceRenderer"; +import { type Name, type Namer, funPrefixNamer } from "../../Naming"; +import { type RenderContext } from "../../Renderer"; +import { type OptionValues } from "../../RendererOptions"; +import { type Sourcelike, modifySource } from "../../Source"; +import { stringEscape } from "../../support/Strings"; +import { defined, panic } from "../../support/Support"; +import { type TargetLanguage } from "../../TargetLanguage"; +import { followTargetType } from "../../Transformers"; +import { type ClassProperty, ClassType, EnumType, type Type, UnionType } from "../../Type"; +import { matchType, nullableFromUnion, removeNullFromUnion } from "../../TypeUtils"; + +import { forbiddenPropertyNames, forbiddenTypeNames } from "./constants"; +import { type pythonOptions } from "./language"; +import { classNameStyle, snakeNameStyle } from "./utils"; + +export class PythonRenderer extends ConvenienceRenderer { + private readonly imports: Map> = new Map(); + + private readonly declaredTypes: Set = new Set(); + + public constructor( + targetLanguage: TargetLanguage, + renderContext: RenderContext, + protected readonly pyOptions: OptionValues + ) { + super(targetLanguage, renderContext); + } + + protected forbiddenNamesForGlobalNamespace(): readonly string[] { + return forbiddenTypeNames; + } + + protected forbiddenForObjectProperties(_: ClassType, _classNamed: Name): ForbiddenWordsInfo { + return { names: forbiddenPropertyNames as unknown as string[], includeGlobalForbidden: false }; + } + + protected makeNamedTypeNamer(): Namer { + return funPrefixNamer("type", classNameStyle); + } + + protected namerForObjectProperty(): Namer { + return funPrefixNamer("property", s => snakeNameStyle(s, false, this.pyOptions.nicePropertyNames)); + } + + protected makeUnionMemberNamer(): null { + return null; + } + + protected makeEnumCaseNamer(): Namer { + return funPrefixNamer("enum-case", s => snakeNameStyle(s, true, this.pyOptions.nicePropertyNames)); + } + + protected get commentLineStart(): string { + return "# "; + } + + protected emitDescriptionBlock(lines: Sourcelike[]): void { + if (lines.length === 1) { + const docstring = modifySource(content => { + if (content.endsWith('"')) { + return content.slice(0, -1) + '\\"'; + } + + return content; + }, lines[0]); + this.emitComments([{ customLines: [docstring], lineStart: '"""', lineEnd: '"""' }]); + } else { + this.emitCommentLines(lines, { + firstLineStart: '"""', + lineStart: "", + afterComment: '"""' + }); + } + } + + protected get needsTypeDeclarationBeforeUse(): boolean { + return true; + } + + protected canBeForwardDeclared(t: Type): boolean { + const kind = t.kind; + return kind === "class" || kind === "enum"; + } + + protected emitBlock(line: Sourcelike, f: () => void): void { + this.emitLine(line); + this.indent(f); + } + + protected string(s: string): Sourcelike { + const openQuote = '"'; + return [openQuote, stringEscape(s), '"']; + } + + protected withImport(module: string, name: string): Sourcelike { + if (this.pyOptions.features.typeHints || module !== "typing") { + // FIXME: This is ugly. We should rather not generate that import in the first + // place, but right now we just make the type source and then throw it away. It's + // not a performance issue, so it's fine, I just bemoan this special case, and + // potential others down the road. + mapUpdateInto(this.imports, module, s => (s ? setUnionInto(s, [name]) : new Set([name]))); + } + + return name; + } + + protected withTyping(name: string): Sourcelike { + return this.withImport("typing", name); + } + + protected namedType(t: Type): Sourcelike { + const name = this.nameForNamedType(t); + if (this.declaredTypes.has(t)) return name; + return ["'", name, "'"]; + } + + protected pythonType(t: Type, _isRootTypeDef = false): Sourcelike { + const actualType = followTargetType(t); + + return matchType( + actualType, + _anyType => this.withTyping("Any"), + _nullType => "None", + _boolType => "bool", + _integerType => "int", + _doubletype => "float", + _stringType => "str", + arrayType => [this.withTyping("List"), "[", this.pythonType(arrayType.items), "]"], + classType => this.namedType(classType), + mapType => [this.withTyping("Dict"), "[str, ", this.pythonType(mapType.values), "]"], + enumType => this.namedType(enumType), + unionType => { + const [hasNull, nonNulls] = removeNullFromUnion(unionType); + const memberTypes = Array.from(nonNulls).map(m => this.pythonType(m)); + + if (hasNull !== null) { + let rest: string[] = []; + if (!this.getAlphabetizeProperties() && this.pyOptions.features.dataClasses && _isRootTypeDef) { + // Only push "= None" if this is a root level type def + // otherwise we may get type defs like List[Optional[int] = None] + // which are invalid + rest.push(" = None"); + } + + if (nonNulls.size > 1) { + this.withImport("typing", "Union"); + return [ + this.withTyping("Optional"), + "[Union[", + arrayIntercalate(", ", memberTypes), + "]]", + ...rest + ]; + } else { + return [this.withTyping("Optional"), "[", defined(iterableFirst(memberTypes)), "]", ...rest]; + } + } else { + return [this.withTyping("Union"), "[", arrayIntercalate(", ", memberTypes), "]"]; + } + }, + transformedStringType => { + if (transformedStringType.kind === "date-time") { + return this.withImport("datetime", "datetime"); + } + + if (transformedStringType.kind === "uuid") { + return this.withImport("uuid", "UUID"); + } + + return panic(`Transformed type ${transformedStringType.kind} not supported`); + } + ); + } + + protected declarationLine(t: Type): Sourcelike { + if (t instanceof ClassType) { + return ["class ", this.nameForNamedType(t), ":"]; + } + + if (t instanceof EnumType) { + return ["class ", this.nameForNamedType(t), "(", this.withImport("enum", "Enum"), "):"]; + } + + return panic(`Can't declare type ${t.kind}`); + } + + protected declareType(t: T, emitter: () => void): void { + this.emitBlock(this.declarationLine(t), () => { + this.emitDescription(this.descriptionForType(t)); + emitter(); + }); + this.declaredTypes.add(t); + } + + protected emitClassMembers(t: ClassType): void { + if (this.pyOptions.features.dataClasses) return; + + const args: Sourcelike[] = []; + this.forEachClassProperty(t, "none", (name, _, cp) => { + args.push([name, this.typeHint(": ", this.pythonType(cp.type))]); + }); + this.emitBlock( + ["def __init__(self, ", arrayIntercalate(", ", args), ")", this.typeHint(" -> None"), ":"], + () => { + if (args.length === 0) { + this.emitLine("pass"); + } else { + this.forEachClassProperty(t, "none", name => { + this.emitLine("self.", name, " = ", name); + }); + } + } + ); + } + + protected typeHint(...sl: Sourcelike[]): Sourcelike { + if (this.pyOptions.features.typeHints) { + return sl; + } + + return []; + } + + protected typingDecl(name: Sourcelike, type: string): Sourcelike { + return [name, this.typeHint(": ", this.withTyping(type))]; + } + + protected typingReturn(type: string): Sourcelike { + return this.typeHint(" -> ", this.withTyping(type)); + } + + protected sortClassProperties( + properties: ReadonlyMap, + propertyNames: ReadonlyMap + ): ReadonlyMap { + if (this.pyOptions.features.dataClasses) { + return mapSortBy(properties, (p: ClassProperty) => { + return (p.type instanceof UnionType && nullableFromUnion(p.type) != null) || p.isOptional ? 1 : 0; + }); + } else { + return super.sortClassProperties(properties, propertyNames); + } + } + + protected emitClass(t: ClassType): void { + if (this.pyOptions.features.dataClasses) { + this.emitLine("@", this.withImport("dataclasses", "dataclass")); + } + + this.declareType(t, () => { + if (this.pyOptions.features.typeHints) { + if (t.getProperties().size === 0) { + this.emitLine("pass"); + } else { + this.forEachClassProperty(t, "none", (name, jsonName, cp) => { + this.emitLine(name, this.typeHint(": ", this.pythonType(cp.type, true))); + this.emitDescription(this.descriptionForClassProperty(t, jsonName)); + }); + } + + this.ensureBlankLine(); + } + + this.emitClassMembers(t); + }); + } + + protected emitEnum(t: EnumType): void { + this.declareType(t, () => { + this.forEachEnumCase(t, "none", (name, jsonName) => { + this.emitLine([name, " = ", this.string(jsonName)]); + }); + }); + } + + protected emitImports(): void { + this.imports.forEach((names, module) => { + this.emitLine("from ", module, " import ", Array.from(names).join(", ")); + }); + } + + protected emitSupportCode(): void { + return; + } + + protected emitClosingCode(): void { + return; + } + + protected emitSourceStructure(_givenOutputFilename: string): void { + const declarationLines = this.gatherSource(() => { + this.forEachNamedType( + ["interposing", 2], + (c: ClassType) => this.emitClass(c), + e => this.emitEnum(e), + _u => { + return; + } + ); + }); + + const closingLines = this.gatherSource(() => this.emitClosingCode()); + const supportLines = this.gatherSource(() => this.emitSupportCode()); + + if (this.leadingComments !== undefined) { + this.emitComments(this.leadingComments); + } + + this.ensureBlankLine(); + this.emitImports(); + this.ensureBlankLine(2); + this.emitGatheredSource(supportLines); + this.ensureBlankLine(2); + this.emitGatheredSource(declarationLines); + this.ensureBlankLine(2); + this.emitGatheredSource(closingLines); + } +} diff --git a/packages/quicktype-core/src/language/Python/constants.ts b/packages/quicktype-core/src/language/Python/constants.ts new file mode 100644 index 000000000..0e725bf50 --- /dev/null +++ b/packages/quicktype-core/src/language/Python/constants.ts @@ -0,0 +1,59 @@ +export const forbiddenTypeNames = [ + "Any", + "True", + "False", + "None", + "Enum", + "List", + "Dict", + "Optional", + "Union", + "Iterable", + "Type", + "TypeVar", + "T", + "EnumT" +] as const; + +export const forbiddenPropertyNames = [ + "and", + "as", + "assert", + "async", + "await", + "bool", + "break", + "class", + "continue", + "datetime", + "def", + "del", + "dict", + "elif", + "else", + "except", + "finally", + "float", + "for", + "from", + "global", + "if", + "import", + "in", + "int", + "is", + "lambda", + "nonlocal", + "not", + "or", + "pass", + "print", + "raise", + "return", + "self", + "str", + "try", + "while", + "with", + "yield" +] as const; diff --git a/packages/quicktype-core/src/language/Python/index.ts b/packages/quicktype-core/src/language/Python/index.ts new file mode 100644 index 000000000..7c7b92ba3 --- /dev/null +++ b/packages/quicktype-core/src/language/Python/index.ts @@ -0,0 +1,3 @@ +export { PythonTargetLanguage, pythonOptions } from "./language"; +export { PythonRenderer } from "./PythonRenderer"; +export { JSONPythonRenderer } from "./JSONPythonRenderer"; diff --git a/packages/quicktype-core/src/language/Python/language.ts b/packages/quicktype-core/src/language/Python/language.ts new file mode 100644 index 000000000..e6aac1d87 --- /dev/null +++ b/packages/quicktype-core/src/language/Python/language.ts @@ -0,0 +1,74 @@ +import { iterableSome } from "collection-utils"; + +import { type RenderContext } from "../../Renderer"; +import { BooleanOption, EnumOption, type Option, getOptionValues } from "../../RendererOptions"; +import { TargetLanguage } from "../../TargetLanguage"; +import { type PrimitiveStringTypeKind, type TransformedStringTypeKind, type Type, UnionType } from "../../Type"; +import { type StringTypeMapping } from "../../TypeBuilder"; +import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../../types"; + +import { JSONPythonRenderer } from "./JSONPythonRenderer"; +import { PythonRenderer } from "./PythonRenderer"; + +export interface PythonFeatures { + dataClasses: boolean; + typeHints: boolean; +} + +export const pythonOptions = { + features: new EnumOption( + "python-version", + "Python version", + [ + ["3.5", { typeHints: false, dataClasses: false }], + ["3.6", { typeHints: true, dataClasses: false }], + ["3.7", { typeHints: true, dataClasses: true }] + ], + "3.6" + ), + justTypes: new BooleanOption("just-types", "Classes only", false), + nicePropertyNames: new BooleanOption("nice-property-names", "Transform property names to be Pythonic", true) +}; + +export class PythonTargetLanguage extends TargetLanguage { + protected getOptions(): Array> { + return [pythonOptions.features, pythonOptions.justTypes, pythonOptions.nicePropertyNames]; + } + + public get stringTypeMapping(): StringTypeMapping { + const mapping: Map = new Map(); + const dateTimeType = "date-time"; + mapping.set("date", dateTimeType); + mapping.set("time", dateTimeType); + mapping.set("date-time", dateTimeType); + mapping.set("uuid", "uuid"); + mapping.set("integer-string", "integer-string"); + mapping.set("bool-string", "bool-string"); + return mapping; + } + + public get supportsUnionsWithBothNumberTypes(): boolean { + return true; + } + + public get supportsOptionalClassProperties(): boolean { + return false; + } + + public needsTransformerForType(t: Type): boolean { + if (t instanceof UnionType) { + return iterableSome(t.members, m => this.needsTransformerForType(m)); + } + + return t.kind === "integer-string" || t.kind === "bool-string"; + } + + protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): PythonRenderer { + const options = getOptionValues(pythonOptions, untypedOptionValues); + if (options.justTypes) { + return new PythonRenderer(this, renderContext, options); + } else { + return new JSONPythonRenderer(this, renderContext, options); + } + } +} diff --git a/packages/quicktype-core/src/language/Python/utils.ts b/packages/quicktype-core/src/language/Python/utils.ts new file mode 100644 index 000000000..6ffb8c5a2 --- /dev/null +++ b/packages/quicktype-core/src/language/Python/utils.ts @@ -0,0 +1,76 @@ +import unicode from "unicode-properties"; + +import { + allLowerWordStyle, + allUpperWordStyle, + combineWords, + firstUpperWordStyle, + originalWord, + splitIntoWords, + utf16LegalizeCharacters +} from "../../support/Strings"; + +function isNormalizedStartCharacter3(utf16Unit: number): boolean { + // FIXME: add Other_ID_Start - https://docs.python.org/3/reference/lexical_analysis.html#identifiers + const category: string = unicode.getCategory(utf16Unit); + return ["Lu", "Ll", "Lt", "Lm", "Lo", "Nl"].includes(category); +} + +function isNormalizedPartCharacter3(utf16Unit: number): boolean { + // FIXME: add Other_ID_Continue - https://docs.python.org/3/reference/lexical_analysis.html#identifiers + if (isNormalizedStartCharacter3(utf16Unit)) return true; + const category: string = unicode.getCategory(utf16Unit); + return ["Mn", "Mc", "Nd", "Pc"].includes(category); +} + +function isStartCharacter3(utf16Unit: number): boolean { + const s = String.fromCharCode(utf16Unit).normalize("NFKC"); + const l = s.length; + if (l === 0 || !isNormalizedStartCharacter3(s.charCodeAt(0))) return false; + for (let i = 1; i < l; i++) { + if (!isNormalizedPartCharacter3(s.charCodeAt(i))) return false; + } + + return true; +} + +function isPartCharacter3(utf16Unit: number): boolean { + const s = String.fromCharCode(utf16Unit).normalize("NFKC"); + const l = s.length; + for (let i = 0; i < l; i++) { + if (!isNormalizedPartCharacter3(s.charCodeAt(i))) return false; + } + + return true; +} + +const legalizeName3 = utf16LegalizeCharacters(isPartCharacter3); + +export function classNameStyle(original: string): string { + const words = splitIntoWords(original); + return combineWords( + words, + legalizeName3, + firstUpperWordStyle, + firstUpperWordStyle, + allUpperWordStyle, + allUpperWordStyle, + "", + isStartCharacter3 + ); +} + +function getWordStyle(uppercase: boolean, forceSnakeNameStyle: boolean) { + if (!forceSnakeNameStyle) { + return originalWord; + } + + return uppercase ? allUpperWordStyle : allLowerWordStyle; +} + +export function snakeNameStyle(original: string, uppercase: boolean, forceSnakeNameStyle: boolean): string { + const wordStyle = getWordStyle(uppercase, forceSnakeNameStyle); + const separator = forceSnakeNameStyle ? "_" : ""; + const words = splitIntoWords(original); + return combineWords(words, legalizeName3, wordStyle, wordStyle, wordStyle, wordStyle, separator, isStartCharacter3); +} diff --git a/packages/quicktype-core/src/language/ruby/index.ts b/packages/quicktype-core/src/language/Ruby/RubyRenderer.ts similarity index 87% rename from packages/quicktype-core/src/language/ruby/index.ts rename to packages/quicktype-core/src/language/Ruby/RubyRenderer.ts index 42466baba..081139193 100644 --- a/packages/quicktype-core/src/language/ruby/index.ts +++ b/packages/quicktype-core/src/language/Ruby/RubyRenderer.ts @@ -1,32 +1,10 @@ -import * as unicode from "unicode-properties"; - import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../../ConvenienceRenderer"; import { type Name, Namer } from "../../Naming"; import { type RenderContext } from "../../Renderer"; -import { - BooleanOption, - EnumOption, - type Option, - type OptionValues, - StringOption, - getOptionValues -} from "../../RendererOptions"; +import { type OptionValues } from "../../RendererOptions"; import { type Sourcelike, modifySource } from "../../Source"; -import { - allLowerWordStyle, - allUpperWordStyle, - combineWords, - escapeNonPrintableMapper, - firstUpperWordStyle, - intToHex, - isLetterOrUnderscore, - isPrintable, - legalizeCharacters, - snakeCase, - splitIntoWords, - utf32ConcatMap -} from "../../support/Strings"; -import { TargetLanguage } from "../../TargetLanguage"; +import { snakeCase } from "../../support/Strings"; +import { type TargetLanguage } from "../../TargetLanguage"; import { ArrayType, type ClassProperty, @@ -36,97 +14,11 @@ import { type Type, type UnionType } from "../../Type"; -import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../../types"; import { matchType, nullableFromUnion, removeNullFromUnion } from "../../TypeUtils"; -import * as keywords from "./keywords"; - -const forbiddenForObjectProperties = Array.from(new Set([...keywords.keywords, ...keywords.reservedProperties])); - -function unicodeEscape(codePoint: number): string { - return "\\u{" + intToHex(codePoint, 0) + "}"; -} - -const stringEscape = utf32ConcatMap(escapeNonPrintableMapper(isPrintable, unicodeEscape)); - -export enum Strictness { - Coercible = "Coercible::", - None = "Types::", - Strict = "Strict::" -} - -export const rubyOptions = { - justTypes: new BooleanOption("just-types", "Plain types only", false), - strictness: new EnumOption("strictness", "Type strictness", [ - ["strict", Strictness.Strict], - ["coercible", Strictness.Coercible], - ["none", Strictness.None] - ]), - namespace: new StringOption("namespace", "Specify a wrapping Namespace", "NAME", "", "secondary") -}; - -export class RubyTargetLanguage extends TargetLanguage { - public constructor() { - super("Ruby", ["ruby"], "rb"); - } - - protected getOptions(): Array> { - return [rubyOptions.justTypes, rubyOptions.strictness, rubyOptions.namespace]; - } - - public get supportsOptionalClassProperties(): boolean { - return true; - } - - protected get defaultIndentation(): string { - return " "; - } - - protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): RubyRenderer { - return new RubyRenderer(this, renderContext, getOptionValues(rubyOptions, untypedOptionValues)); - } -} - -const isStartCharacter = isLetterOrUnderscore; - -function isPartCharacter(utf16Unit: number): boolean { - const category: string = unicode.getCategory(utf16Unit); - return ["Nd", "Pc", "Mn", "Mc"].includes(category) || isStartCharacter(utf16Unit); -} - -const legalizeName = legalizeCharacters(isPartCharacter); - -function simpleNameStyle(original: string, uppercase: boolean): string { - if (/^[0-9]+$/.test(original)) { - original = original + "N"; - } - - const words = splitIntoWords(original); - return combineWords( - words, - legalizeName, - uppercase ? firstUpperWordStyle : allLowerWordStyle, - uppercase ? firstUpperWordStyle : allLowerWordStyle, - allUpperWordStyle, - allUpperWordStyle, - "", - isStartCharacter - ); -} - -function memberNameStyle(original: string): string { - const words = splitIntoWords(original); - return combineWords( - words, - legalizeName, - allLowerWordStyle, - allLowerWordStyle, - allLowerWordStyle, - allLowerWordStyle, - "_", - isStartCharacter - ); -} +import { globals } from "./constants"; +import { type rubyOptions } from "./language"; +import { Strictness, forbiddenForObjectProperties, memberNameStyle, simpleNameStyle, stringEscape } from "./utils"; export class RubyRenderer extends ConvenienceRenderer { public constructor( @@ -149,8 +41,8 @@ export class RubyRenderer extends ConvenienceRenderer { return "class" === t.kind; } - protected forbiddenNamesForGlobalNamespace(): string[] { - return keywords.globals.concat(["Types", "JSON", "Dry", "Constructor", "Self"]); + protected forbiddenNamesForGlobalNamespace(): readonly string[] { + return [...globals, "Types", "JSON", "Dry", "Constructor", "Self"] as const; } protected forbiddenForObjectProperties(_c: ClassType, _classNamed: Name): ForbiddenWordsInfo { diff --git a/packages/quicktype-core/src/language/ruby/keywords.ts b/packages/quicktype-core/src/language/Ruby/constants.ts similarity index 97% rename from packages/quicktype-core/src/language/ruby/keywords.ts rename to packages/quicktype-core/src/language/Ruby/constants.ts index 0229d9044..9848048f5 100644 --- a/packages/quicktype-core/src/language/ruby/keywords.ts +++ b/packages/quicktype-core/src/language/Ruby/constants.ts @@ -39,8 +39,8 @@ export const keywords = [ "until", "when", "while", - "yield", -]; + "yield" +] as const; const globalClasses = [ "ArgumentError", @@ -133,8 +133,8 @@ const globalClasses = [ "Undefined", "UnicodeNormalize", "Warning", - "ZeroDivisionError", -]; + "ZeroDivisionError" +] as const; const kernel = [ "__callee__", @@ -286,10 +286,10 @@ const kernel = [ "untrace_var", "untrust", "untrusted?", - "warn", -]; + "warn" +] as const; -export const globals = kernel.concat(globalClasses); +export const globals = [...kernel, ...globalClasses] as const; export const reservedProperties = [ "__id__", @@ -355,5 +355,5 @@ export const reservedProperties = [ "undef", "untrust", "while", - "with", -]; + "with" +] as const; diff --git a/packages/quicktype-core/src/language/Ruby/index.ts b/packages/quicktype-core/src/language/Ruby/index.ts new file mode 100644 index 000000000..13ba45f36 --- /dev/null +++ b/packages/quicktype-core/src/language/Ruby/index.ts @@ -0,0 +1,2 @@ +export { RubyTargetLanguage, rubyOptions } from "./language"; +export { RubyRenderer } from "./RubyRenderer"; diff --git a/packages/quicktype-core/src/language/Ruby/language.ts b/packages/quicktype-core/src/language/Ruby/language.ts new file mode 100644 index 000000000..e85598008 --- /dev/null +++ b/packages/quicktype-core/src/language/Ruby/language.ts @@ -0,0 +1,39 @@ +import { type RenderContext } from "../../Renderer"; +import { BooleanOption, EnumOption, type Option, StringOption, getOptionValues } from "../../RendererOptions"; +import { TargetLanguage } from "../../TargetLanguage"; +import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../../types"; + +import { RubyRenderer } from "./RubyRenderer"; +import { Strictness } from "./utils"; + +export const rubyOptions = { + justTypes: new BooleanOption("just-types", "Plain types only", false), + strictness: new EnumOption("strictness", "Type strictness", [ + ["strict", Strictness.Strict], + ["coercible", Strictness.Coercible], + ["none", Strictness.None] + ]), + namespace: new StringOption("namespace", "Specify a wrapping Namespace", "NAME", "", "secondary") +}; + +export class RubyTargetLanguage extends TargetLanguage { + public constructor() { + super("Ruby", ["ruby"], "rb"); + } + + protected getOptions(): Array> { + return [rubyOptions.justTypes, rubyOptions.strictness, rubyOptions.namespace]; + } + + public get supportsOptionalClassProperties(): boolean { + return true; + } + + protected get defaultIndentation(): string { + return " "; + } + + protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): RubyRenderer { + return new RubyRenderer(this, renderContext, getOptionValues(rubyOptions, untypedOptionValues)); + } +} diff --git a/packages/quicktype-core/src/language/Ruby/utils.ts b/packages/quicktype-core/src/language/Ruby/utils.ts new file mode 100644 index 000000000..1b78d957c --- /dev/null +++ b/packages/quicktype-core/src/language/Ruby/utils.ts @@ -0,0 +1,71 @@ +import unicode from "unicode-properties"; + +import { + allLowerWordStyle, + allUpperWordStyle, + combineWords, + escapeNonPrintableMapper, + firstUpperWordStyle, + intToHex, + isLetterOrUnderscore, + isPrintable, + legalizeCharacters, + splitIntoWords, + utf32ConcatMap +} from "../../support/Strings"; + +import * as keywords from "./constants"; + +export enum Strictness { + Strict = "Strict::", + Coercible = "Coercible::", + None = "Types::" +} + +export const forbiddenForObjectProperties = Array.from(new Set([...keywords.keywords, ...keywords.reservedProperties])); +function unicodeEscape(codePoint: number): string { + return "\\u{" + intToHex(codePoint, 0) + "}"; +} + +export const stringEscape = utf32ConcatMap(escapeNonPrintableMapper(isPrintable, unicodeEscape)); + +const isStartCharacter = isLetterOrUnderscore; + +function isPartCharacter(utf16Unit: number): boolean { + const category: string = unicode.getCategory(utf16Unit); + return ["Nd", "Pc", "Mn", "Mc"].includes(category) || isStartCharacter(utf16Unit); +} + +const legalizeName = legalizeCharacters(isPartCharacter); + +export function simpleNameStyle(original: string, uppercase: boolean): string { + if (/^[0-9]+$/.test(original)) { + original = original + "N"; + } + + const words = splitIntoWords(original); + return combineWords( + words, + legalizeName, + uppercase ? firstUpperWordStyle : allLowerWordStyle, + uppercase ? firstUpperWordStyle : allLowerWordStyle, + allUpperWordStyle, + allUpperWordStyle, + "", + isStartCharacter + ); +} + +export function memberNameStyle(original: string): string { + const words = splitIntoWords(original); + return combineWords( + words, + legalizeName, + allLowerWordStyle, + allLowerWordStyle, + allLowerWordStyle, + allLowerWordStyle, + "_", + isStartCharacter + ); +} diff --git a/packages/quicktype-core/src/language/Rust.ts b/packages/quicktype-core/src/language/Rust/RustRenderer.ts similarity index 54% rename from packages/quicktype-core/src/language/Rust.ts rename to packages/quicktype-core/src/language/Rust/RustRenderer.ts index 8dcf5b93a..6e0a6653f 100644 --- a/packages/quicktype-core/src/language/Rust.ts +++ b/packages/quicktype-core/src/language/Rust/RustRenderer.ts @@ -1,267 +1,30 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { mapFirst } from "collection-utils"; -import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../Annotation"; -import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../ConvenienceRenderer"; -import { type Name, type Namer, funPrefixNamer } from "../Naming"; -import { type RenderContext } from "../Renderer"; -import { BooleanOption, EnumOption, type Option, type OptionValues, getOptionValues } from "../RendererOptions"; -import { type Sourcelike, maybeAnnotated } from "../Source"; +import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../../Annotation"; +import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../../ConvenienceRenderer"; +import { type Name, type Namer } from "../../Naming"; +import { type RenderContext } from "../../Renderer"; +import { type OptionValues } from "../../RendererOptions"; +import { type Sourcelike, maybeAnnotated } from "../../Source"; +import { defined } from "../../support/Support"; +import { type TargetLanguage } from "../../TargetLanguage"; +import { type ClassType, type EnumType, type Type, UnionType } from "../../Type"; +import { matchType, nullableFromUnion, removeNullFromUnion } from "../../TypeUtils"; + +import { keywords } from "./constants"; +import { type rustOptions } from "./language"; import { - allLowerWordStyle, - combineWords, - escapeNonPrintableMapper, - firstUpperWordStyle, - intToHex, - isAscii, - isLetterOrUnderscore, - isLetterOrUnderscoreOrDigit, - isPrintable, - legalizeCharacters, - splitIntoWords, - utf32ConcatMap -} from "../support/Strings"; -import { defined } from "../support/Support"; -import { TargetLanguage } from "../TargetLanguage"; -import { type ClassType, type EnumType, type Type, UnionType } from "../Type"; -import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../types"; -import { matchType, nullableFromUnion, removeNullFromUnion } from "../TypeUtils"; - -export enum Density { - Normal = "Normal", - Dense = "Dense" -} - -export enum Visibility { - Private = "Private", - Crate = "Crate", - Public = "Public" -} - -export const rustOptions = { - density: new EnumOption("density", "Density", [ - ["normal", Density.Normal], - ["dense", Density.Dense] - ]), - visibility: new EnumOption("visibility", "Field visibility", [ - ["private", Visibility.Private], - ["crate", Visibility.Crate], - ["public", Visibility.Public] - ]), - deriveDebug: new BooleanOption("derive-debug", "Derive Debug impl", false), - deriveClone: new BooleanOption("derive-clone", "Derive Clone impl", false), - derivePartialEq: new BooleanOption("derive-partial-eq", "Derive PartialEq impl", false), - skipSerializingNone: new BooleanOption("skip-serializing-none", "Skip serializing empty Option fields", false), - edition2018: new BooleanOption("edition-2018", "Edition 2018", true), - leadingComments: new BooleanOption("leading-comments", "Leading Comments", true) -}; - -type NameToParts = (name: string) => string[]; -type PartsToName = (parts: string[]) => string; -interface NamingStyle { - fromParts: PartsToName; - regex: RegExp; - toParts: NameToParts; -} - -const namingStyles: Record = { - snake_case: { - regex: /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/, - toParts: (name: string): string[] => name.split("_"), - fromParts: (parts: string[]): string => parts.map(p => p.toLowerCase()).join("_") - }, - SCREAMING_SNAKE_CASE: { - regex: /^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$/, - toParts: (name: string): string[] => name.split("_"), - fromParts: (parts: string[]): string => parts.map(p => p.toUpperCase()).join("_") - }, - camelCase: { - regex: /^[a-z]+([A-Z0-9][a-z]*)*$/, - toParts: (name: string): string[] => namingStyles.snake_case.toParts(name.replace(/(.)([A-Z])/g, "$1_$2")), - fromParts: (parts: string[]): string => - parts - .map((p, i) => - i === 0 ? p.toLowerCase() : p.substring(0, 1).toUpperCase() + p.substring(1).toLowerCase() - ) - .join("") - }, - PascalCase: { - regex: /^[A-Z][a-z]*([A-Z0-9][a-z]*)*$/, - toParts: (name: string): string[] => namingStyles.snake_case.toParts(name.replace(/(.)([A-Z])/g, "$1_$2")), - fromParts: (parts: string[]): string => - parts.map(p => p.substring(0, 1).toUpperCase() + p.substring(1).toLowerCase()).join("") - }, - "kebab-case": { - regex: /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/, - toParts: (name: string): string[] => name.split("-"), - fromParts: (parts: string[]): string => parts.map(p => p.toLowerCase()).join("-") - }, - "SCREAMING-KEBAB-CASE": { - regex: /^[A-Z][A-Z0-9]*(-[A-Z0-9]+)*$/, - toParts: (name: string): string[] => name.split("-"), - fromParts: (parts: string[]): string => parts.map(p => p.toUpperCase()).join("-") - }, - lowercase: { - regex: /^[a-z][a-z0-9]*$/, - toParts: (name: string): string[] => [name], - fromParts: (parts: string[]): string => parts.map(p => p.toLowerCase()).join("") - }, - UPPERCASE: { - regex: /^[A-Z][A-Z0-9]*$/, - toParts: (name: string): string[] => [name], - fromParts: (parts: string[]): string => parts.map(p => p.toUpperCase()).join("") - } -}; - -export class RustTargetLanguage extends TargetLanguage { - protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): RustRenderer { - return new RustRenderer(this, renderContext, getOptionValues(rustOptions, untypedOptionValues)); - } - - public constructor() { - super("Rust", ["rust", "rs", "rustlang"], "rs"); - } - - protected getOptions(): Array> { - return [ - rustOptions.density, - rustOptions.visibility, - rustOptions.deriveDebug, - rustOptions.deriveClone, - rustOptions.derivePartialEq, - rustOptions.edition2018, - rustOptions.leadingComments, - rustOptions.skipSerializingNone - ]; - } -} - -const keywords = [ - "Serialize", - "Deserialize", - - // Special reserved identifiers used internally for elided lifetimes, - // unnamed method parameters, crate root module, error recovery etc. - "{{root}}", - "$crate", - - // Keywords used in the language. - "as", - "async", - "box", - "break", - "const", - "continue", - "crate", - "else", - "enum", - "extern", - "false", - "fn", - "for", - "if", - "impl", - "in", - "let", - "loop", - "match", - "mod", - "move", - "mut", - "pub", - "ref", - "return", - "self", - "Self", - "static", - "struct", - "super", - "trait", - "true", - "type", - "unsafe", - "use", - "where", - "while", - - // Keywords reserved for future use. - "abstract", - "alignof", - "become", - "do", - "final", - "macro", - "offsetof", - "override", - "priv", - "proc", - "pure", - "sizeof", - "typeof", - "unsized", - "virtual", - "yield", - - // Weak keywords, have special meaning only in specific contexts. - "catch", - "default", - "dyn", - "'static", - "union", - - // Conflict between `std::Option` and potentially generated Option - "option" -]; - -const isAsciiLetterOrUnderscoreOrDigit = (codePoint: number): boolean => { - if (!isAscii(codePoint)) { - return false; - } - - return isLetterOrUnderscoreOrDigit(codePoint); -}; - -const isAsciiLetterOrUnderscore = (codePoint: number): boolean => { - if (!isAscii(codePoint)) { - return false; - } - - return isLetterOrUnderscore(codePoint); -}; - -const legalizeName = legalizeCharacters(isAsciiLetterOrUnderscoreOrDigit); - -function rustStyle(original: string, isSnakeCase: boolean): string { - const words = splitIntoWords(original); - - const wordStyle = isSnakeCase ? allLowerWordStyle : firstUpperWordStyle; - - const combined = combineWords( - words, - legalizeName, - wordStyle, - wordStyle, - wordStyle, - wordStyle, - isSnakeCase ? "_" : "", - isAsciiLetterOrUnderscore - ); - - return combined === "_" ? "_underscore" : combined; -} - -const snakeNamingFunction = funPrefixNamer("default", (original: string) => rustStyle(original, true)); -const camelNamingFunction = funPrefixNamer("camel", (original: string) => rustStyle(original, false)); - -const standardUnicodeRustEscape = (codePoint: number): string => { - if (codePoint <= 0xffff) { - return "\\u{" + intToHex(codePoint, 4) + "}"; - } else { - return "\\u{" + intToHex(codePoint, 6) + "}"; - } -}; - -const rustStringEscape = utf32ConcatMap(escapeNonPrintableMapper(isPrintable, standardUnicodeRustEscape)); + Density, + Visibility, + camelNamingFunction, + getPreferedNamingStyle, + listMatchingNamingStyles, + nameToNamingStyle, + namingStyles, + rustStringEscape, + snakeNamingFunction +} from "./utils"; export class RustRenderer extends ConvenienceRenderer { public constructor( @@ -288,7 +51,7 @@ export class RustRenderer extends ConvenienceRenderer { return camelNamingFunction; } - protected forbiddenNamesForGlobalNamespace(): string[] { + protected forbiddenNamesForGlobalNamespace(): readonly string[] { return keywords; } @@ -548,36 +311,3 @@ export class RustRenderer extends ConvenienceRenderer { ); } } - -function getPreferedNamingStyle(namingStyleOccurences: string[], defaultStyle: string): string { - const occurrences = Object.fromEntries(Object.keys(namingStyles).map(key => [key, 0])); - namingStyleOccurences.forEach(style => ++occurrences[style]); - const max = Math.max(...Object.values(occurrences)); - const preferedStyles = Object.entries(occurrences) - .filter(([_style, num]) => num === max) - .map(([style, _num]) => style); - if (preferedStyles.includes(defaultStyle)) { - return defaultStyle; - } - - return preferedStyles[0]; -} - -function listMatchingNamingStyles(name: string): string[] { - return Object.entries(namingStyles) - .filter(([_, { regex }]) => regex.test(name)) - .map(([namingStyle, _]) => namingStyle); -} - -function nameToNamingStyle(name: string, style: string): string { - if (namingStyles[style].regex.test(name)) { - return name; - } - - const fromStyle = listMatchingNamingStyles(name)[0]; - if (fromStyle === undefined) { - return name; - } - - return namingStyles[style].fromParts(namingStyles[fromStyle].toParts(name)); -} diff --git a/packages/quicktype-core/src/language/Rust/constants.ts b/packages/quicktype-core/src/language/Rust/constants.ts new file mode 100644 index 000000000..8ba3d3eb8 --- /dev/null +++ b/packages/quicktype-core/src/language/Rust/constants.ts @@ -0,0 +1,76 @@ +export const keywords = [ + "Serialize", + "Deserialize", + + // Special reserved identifiers used internally for elided lifetimes, + // unnamed method parameters, crate root module, error recovery etc. + "{{root}}", + "$crate", + + // Keywords used in the language. + "as", + "async", + "box", + "break", + "const", + "continue", + "crate", + "else", + "enum", + "extern", + "false", + "fn", + "for", + "if", + "impl", + "in", + "let", + "loop", + "match", + "mod", + "move", + "mut", + "pub", + "ref", + "return", + "self", + "Self", + "static", + "struct", + "super", + "trait", + "true", + "type", + "unsafe", + "use", + "where", + "while", + + // Keywords reserved for future use. + "abstract", + "alignof", + "become", + "do", + "final", + "macro", + "offsetof", + "override", + "priv", + "proc", + "pure", + "sizeof", + "typeof", + "unsized", + "virtual", + "yield", + + // Weak keywords, have special meaning only in specific contexts. + "catch", + "default", + "dyn", + "'static", + "union", + + // Conflict between `std::Option` and potentially generated Option + "option" +] as const; diff --git a/packages/quicktype-core/src/language/Rust/index.ts b/packages/quicktype-core/src/language/Rust/index.ts new file mode 100644 index 000000000..456ae1274 --- /dev/null +++ b/packages/quicktype-core/src/language/Rust/index.ts @@ -0,0 +1,2 @@ +export { RustTargetLanguage, rustOptions } from "./language"; +export { RustRenderer } from "./RustRenderer"; diff --git a/packages/quicktype-core/src/language/Rust/language.ts b/packages/quicktype-core/src/language/Rust/language.ts new file mode 100644 index 000000000..2f62883ea --- /dev/null +++ b/packages/quicktype-core/src/language/Rust/language.ts @@ -0,0 +1,48 @@ +import { type RenderContext } from "../../Renderer"; +import { BooleanOption, EnumOption, type Option, getOptionValues } from "../../RendererOptions"; +import { TargetLanguage } from "../../TargetLanguage"; +import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../../types"; + +import { RustRenderer } from "./RustRenderer"; +import { Density, Visibility } from "./utils"; + +export const rustOptions = { + density: new EnumOption("density", "Density", [ + ["normal", Density.Normal], + ["dense", Density.Dense] + ]), + visibility: new EnumOption("visibility", "Field visibility", [ + ["private", Visibility.Private], + ["crate", Visibility.Crate], + ["public", Visibility.Public] + ]), + deriveDebug: new BooleanOption("derive-debug", "Derive Debug impl", false), + deriveClone: new BooleanOption("derive-clone", "Derive Clone impl", false), + derivePartialEq: new BooleanOption("derive-partial-eq", "Derive PartialEq impl", false), + skipSerializingNone: new BooleanOption("skip-serializing-none", "Skip serializing empty Option fields", false), + edition2018: new BooleanOption("edition-2018", "Edition 2018", true), + leadingComments: new BooleanOption("leading-comments", "Leading Comments", true) +}; + +export class RustTargetLanguage extends TargetLanguage { + protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): RustRenderer { + return new RustRenderer(this, renderContext, getOptionValues(rustOptions, untypedOptionValues)); + } + + public constructor() { + super("Rust", ["rust", "rs", "rustlang"], "rs"); + } + + protected getOptions(): Array> { + return [ + rustOptions.density, + rustOptions.visibility, + rustOptions.deriveDebug, + rustOptions.deriveClone, + rustOptions.derivePartialEq, + rustOptions.edition2018, + rustOptions.leadingComments, + rustOptions.skipSerializingNone + ]; + } +} diff --git a/packages/quicktype-core/src/language/Rust/utils.ts b/packages/quicktype-core/src/language/Rust/utils.ts new file mode 100644 index 000000000..1b086f545 --- /dev/null +++ b/packages/quicktype-core/src/language/Rust/utils.ts @@ -0,0 +1,166 @@ +import { funPrefixNamer } from "../../Naming"; +import { + allLowerWordStyle, + combineWords, + escapeNonPrintableMapper, + firstUpperWordStyle, + intToHex, + isAscii, + isLetterOrUnderscore, + isLetterOrUnderscoreOrDigit, + isPrintable, + legalizeCharacters, + splitIntoWords, + utf32ConcatMap +} from "../../support/Strings"; + +export enum Density { + Normal = "Normal", + Dense = "Dense" +} + +export enum Visibility { + Private = "Private", + Crate = "Crate", + Public = "Public" +} + +type NameToParts = (name: string) => string[]; +type PartsToName = (parts: string[]) => string; +interface NamingStyle { + fromParts: PartsToName; + regex: RegExp; + toParts: NameToParts; +} + +export const namingStyles: Record = { + snake_case: { + regex: /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/, + toParts: (name: string): string[] => name.split("_"), + fromParts: (parts: string[]): string => parts.map(p => p.toLowerCase()).join("_") + }, + SCREAMING_SNAKE_CASE: { + regex: /^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$/, + toParts: (name: string): string[] => name.split("_"), + fromParts: (parts: string[]): string => parts.map(p => p.toUpperCase()).join("_") + }, + camelCase: { + regex: /^[a-z]+([A-Z0-9][a-z]*)*$/, + toParts: (name: string): string[] => namingStyles.snake_case.toParts(name.replace(/(.)([A-Z])/g, "$1_$2")), + fromParts: (parts: string[]): string => + parts + .map((p, i) => + i === 0 ? p.toLowerCase() : p.substring(0, 1).toUpperCase() + p.substring(1).toLowerCase() + ) + .join("") + }, + PascalCase: { + regex: /^[A-Z][a-z]*([A-Z0-9][a-z]*)*$/, + toParts: (name: string): string[] => namingStyles.snake_case.toParts(name.replace(/(.)([A-Z])/g, "$1_$2")), + fromParts: (parts: string[]): string => + parts.map(p => p.substring(0, 1).toUpperCase() + p.substring(1).toLowerCase()).join("") + }, + "kebab-case": { + regex: /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/, + toParts: (name: string): string[] => name.split("-"), + fromParts: (parts: string[]): string => parts.map(p => p.toLowerCase()).join("-") + }, + "SCREAMING-KEBAB-CASE": { + regex: /^[A-Z][A-Z0-9]*(-[A-Z0-9]+)*$/, + toParts: (name: string): string[] => name.split("-"), + fromParts: (parts: string[]): string => parts.map(p => p.toUpperCase()).join("-") + }, + lowercase: { + regex: /^[a-z][a-z0-9]*$/, + toParts: (name: string): string[] => [name], + fromParts: (parts: string[]): string => parts.map(p => p.toLowerCase()).join("") + }, + UPPERCASE: { + regex: /^[A-Z][A-Z0-9]*$/, + toParts: (name: string): string[] => [name], + fromParts: (parts: string[]): string => parts.map(p => p.toUpperCase()).join("") + } +}; + +const isAsciiLetterOrUnderscoreOrDigit = (codePoint: number): boolean => { + if (!isAscii(codePoint)) { + return false; + } + + return isLetterOrUnderscoreOrDigit(codePoint); +}; + +const isAsciiLetterOrUnderscore = (codePoint: number): boolean => { + if (!isAscii(codePoint)) { + return false; + } + + return isLetterOrUnderscore(codePoint); +}; + +const legalizeName = legalizeCharacters(isAsciiLetterOrUnderscoreOrDigit); + +function rustStyle(original: string, isSnakeCase: boolean): string { + const words = splitIntoWords(original); + + const wordStyle = isSnakeCase ? allLowerWordStyle : firstUpperWordStyle; + + const combined = combineWords( + words, + legalizeName, + wordStyle, + wordStyle, + wordStyle, + wordStyle, + isSnakeCase ? "_" : "", + isAsciiLetterOrUnderscore + ); + + return combined === "_" ? "_underscore" : combined; +} + +export const snakeNamingFunction = funPrefixNamer("default", (original: string) => rustStyle(original, true)); +export const camelNamingFunction = funPrefixNamer("camel", (original: string) => rustStyle(original, false)); + +const standardUnicodeRustEscape = (codePoint: number): string => { + if (codePoint <= 0xffff) { + return "\\u{" + intToHex(codePoint, 4) + "}"; + } else { + return "\\u{" + intToHex(codePoint, 6) + "}"; + } +}; + +export const rustStringEscape = utf32ConcatMap(escapeNonPrintableMapper(isPrintable, standardUnicodeRustEscape)); + +export function getPreferedNamingStyle(namingStyleOccurences: string[], defaultStyle: string): string { + const occurrences = Object.fromEntries(Object.keys(namingStyles).map(key => [key, 0])); + namingStyleOccurences.forEach(style => ++occurrences[style]); + const max = Math.max(...Object.values(occurrences)); + const preferedStyles = Object.entries(occurrences) + .filter(([_style, num]) => num === max) + .map(([style, _num]) => style); + if (preferedStyles.includes(defaultStyle)) { + return defaultStyle; + } + + return preferedStyles[0]; +} + +export function listMatchingNamingStyles(name: string): string[] { + return Object.entries(namingStyles) + .filter(([_, { regex }]) => regex.test(name)) + .map(([namingStyle, _]) => namingStyle); +} + +export function nameToNamingStyle(name: string, style: string): string { + if (namingStyles[style].regex.test(name)) { + return name; + } + + const fromStyle = listMatchingNamingStyles(name)[0]; + if (fromStyle === undefined) { + return name; + } + + return namingStyles[style].fromParts(namingStyles[fromStyle].toParts(name)); +} diff --git a/packages/quicktype-core/src/language/Scala3.ts b/packages/quicktype-core/src/language/Scala3.ts deleted file mode 100644 index 75da0773f..000000000 --- a/packages/quicktype-core/src/language/Scala3.ts +++ /dev/null @@ -1,721 +0,0 @@ -import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../Annotation"; -import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../ConvenienceRenderer"; -import { type Name, type Namer, funPrefixNamer } from "../Naming"; -import { type RenderContext } from "../Renderer"; -import { EnumOption, type Option, type OptionValues, StringOption, getOptionValues } from "../RendererOptions"; -import { type Sourcelike, maybeAnnotated } from "../Source"; -import { - allLowerWordStyle, - allUpperWordStyle, - combineWords, - firstUpperWordStyle, - isDigit, - isLetterOrUnderscore, - isNumeric, - legalizeCharacters, - splitIntoWords -} from "../support/Strings"; -import { assertNever } from "../support/Support"; -import { TargetLanguage } from "../TargetLanguage"; -import { - ArrayType, - type ClassProperty, - type ClassType, - type EnumType, - MapType, - type ObjectType, - type Type, - type UnionType -} from "../Type"; -import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../types"; -import { matchType, nullableFromUnion, removeNullFromUnion } from "../TypeUtils"; - -export enum Framework { - None = "None", - Upickle = "Upickle", - Circe = "Circe" -} - -export const scala3Options = { - framework: new EnumOption( - "framework", - "Serialization framework", - [ - ["just-types", Framework.None], - ["circe", Framework.Circe], - ["upickle", Framework.Upickle] - ], - undefined - ), - packageName: new StringOption("package", "Package", "PACKAGE", "quicktype") -}; - -// Use backticks for param names with symbols -const invalidSymbols = [ - ":", - "-", - "+", - "!", - "@", - "#", - "$", - "%", - "^", - "&", - "*", - "(", - ")", - ">", - "<", - "/", - ";", - "'", - '"', - "{", - "}", - ":", - "~", - "`", - "." -]; - -const keywords = [ - "abstract", - "case", - "catch", - "class", - "def", - "do", - "else", - "enum", - "extends", - "export", - "false", - "final", - "finally", - "for", - "forSome", - "if", - "implicit", - "import", - "lazy", - "match", - "new", - "null", - "object", - "override", - "package", - "private", - "protected", - "return", - "sealed", - "super", - "this", - "then", - "throw", - "trait", - "try", - "true", - "type", - "val", - "var", - "while", - "with", - "yield", - "Any", - "Boolean", - "Double", - "Float", - "Long", - "Int", - "Short", - "System", - "Byte", - "String", - "Array", - "List", - "Map", - "Enum" -]; - -/** - * Check if given parameter name should be wrapped in a backtick - * @param paramName - */ -const shouldAddBacktick = (paramName: string): boolean => { - return ( - keywords.some(s => paramName === s) || - invalidSymbols.some(s => paramName.includes(s)) || - !isNaN(+parseFloat(paramName)) || - !isNaN(parseInt(paramName.charAt(0))) - ); -}; - -const wrapOption = (s: string, optional: boolean): string => { - if (optional) { - return "Option[" + s + "]"; - } else { - return s; - } -}; - -function isPartCharacter(codePoint: number): boolean { - return isLetterOrUnderscore(codePoint) || isNumeric(codePoint); -} - -function isStartCharacter(codePoint: number): boolean { - return isPartCharacter(codePoint) && !isDigit(codePoint); -} - -const legalizeName = legalizeCharacters(isPartCharacter); - -function scalaNameStyle(isUpper: boolean, original: string): string { - const words = splitIntoWords(original); - return combineWords( - words, - legalizeName, - isUpper ? firstUpperWordStyle : allLowerWordStyle, - firstUpperWordStyle, - isUpper ? allUpperWordStyle : allLowerWordStyle, - allUpperWordStyle, - "", - isStartCharacter - ); -} - -/* function unicodeEscape(codePoint: number): string { - return "\\u" + intToHex(codePoint, 4); -} */ - -// const _stringEscape = utf32ConcatMap(escapeNonPrintableMapper(isPrintable, unicodeEscape)); - -/* function stringEscape(s: string): string { - // "$this" is a template string in Kotlin so we have to escape $ - return _stringEscape(s).replace(/\$/g, "\\$"); -} */ - -const upperNamingFunction = funPrefixNamer("upper", s => scalaNameStyle(true, s)); -const lowerNamingFunction = funPrefixNamer("lower", s => scalaNameStyle(false, s)); - -export class Scala3Renderer extends ConvenienceRenderer { - public constructor( - targetLanguage: TargetLanguage, - renderContext: RenderContext, - protected readonly _scalaOptions: OptionValues - ) { - super(targetLanguage, renderContext); - } - - protected forbiddenNamesForGlobalNamespace(): string[] { - return keywords; - } - - protected forbiddenForObjectProperties(_: ObjectType, _classNamed: Name): ForbiddenWordsInfo { - return { names: [], includeGlobalForbidden: true }; - } - - protected forbiddenForEnumCases(_: EnumType, _enumName: Name): ForbiddenWordsInfo { - return { names: [], includeGlobalForbidden: true }; - } - - protected forbiddenForUnionMembers(_u: UnionType, _unionName: Name): ForbiddenWordsInfo { - return { names: [], includeGlobalForbidden: false }; - } - - protected topLevelNameStyle(rawName: string): string { - return scalaNameStyle(true, rawName); - } - - protected makeNamedTypeNamer(): Namer { - return upperNamingFunction; - } - - protected namerForObjectProperty(): Namer { - return lowerNamingFunction; - } - - protected makeUnionMemberNamer(): Namer { - return funPrefixNamer("upper", s => scalaNameStyle(true, s) + "Value"); - } - - protected makeEnumCaseNamer(): Namer { - return funPrefixNamer("upper", s => s.replace(" ", "")); // TODO - add backticks where appropriate - } - - protected emitDescriptionBlock(lines: Sourcelike[]): void { - this.emitCommentLines(lines, { lineStart: " * ", beforeComment: "/**", afterComment: " */" }); - } - - protected emitBlock( - line: Sourcelike, - f: () => void, - delimiter: "curly" | "paren" | "lambda" | "none" = "curly" - ): void { - const [open, close] = - delimiter === "curly" - ? ["{", "}"] - : delimiter === "paren" - ? ["(", ")"] - : delimiter === "none" - ? ["", ""] - : ["{", "})"]; - this.emitLine(line, " ", open); - this.indent(f); - this.emitLine(close); - } - - protected anySourceType(optional: boolean): Sourcelike { - return [wrapOption("Any", optional)]; - } - - // (asarazan): I've broken out the following two functions - // because some renderers, such as kotlinx, can cope with `any`, while some get mad. - protected arrayType(arrayType: ArrayType, withIssues = false): Sourcelike { - return ["Seq[", this.scalaType(arrayType.items, withIssues), "]"]; - } - - protected mapType(mapType: MapType, withIssues = false): Sourcelike { - return ["Map[String, ", this.scalaType(mapType.values, withIssues), "]"]; - } - - protected scalaType(t: Type, withIssues = false, noOptional = false): Sourcelike { - return matchType( - t, - _anyType => { - return maybeAnnotated(withIssues, anyTypeIssueAnnotation, this.anySourceType(!noOptional)); - }, - _nullType => { - // return "None.type" - return maybeAnnotated(withIssues, nullTypeIssueAnnotation, this.anySourceType(!noOptional)); - }, - _boolType => "Boolean", - _integerType => "Long", - _doubleType => "Double", - _stringType => "String", - arrayType => this.arrayType(arrayType, withIssues), - classType => this.nameForNamedType(classType), - mapType => this.mapType(mapType, withIssues), - enumType => this.nameForNamedType(enumType), - unionType => { - const nullable = nullableFromUnion(unionType); - if (nullable !== null) { - if (noOptional) { - return [this.scalaType(nullable, withIssues)]; - } else { - return ["Option[", this.scalaType(nullable, withIssues), "]"]; - } - } - - return this.nameForNamedType(unionType); - } - ); - } - - protected emitUsageHeader(): void { - // To be overridden - } - - protected emitHeader(): void { - if (this.leadingComments !== undefined) { - this.emitComments(this.leadingComments); - } else { - this.emitUsageHeader(); - } - - this.ensureBlankLine(); - this.emitLine("package ", this._scalaOptions.packageName); - this.ensureBlankLine(); - } - - protected emitTopLevelArray(t: ArrayType, name: Name): void { - const elementType = this.scalaType(t.items); - this.emitLine(["type ", name, " = List[", elementType, "]"]); - } - - protected emitTopLevelMap(t: MapType, name: Name): void { - const elementType = this.scalaType(t.values); - this.emitLine(["type ", name, " = Map[String, ", elementType, "]"]); - } - - protected emitEmptyClassDefinition(c: ClassType, className: Name): void { - this.emitDescription(this.descriptionForType(c)); - this.emitLine("case class ", className, "()"); - } - - protected emitClassDefinition(c: ClassType, className: Name): void { - if (c.getProperties().size === 0) { - this.emitEmptyClassDefinition(c, className); - return; - } - - const scalaType = (p: ClassProperty): Sourcelike => { - if (p.isOptional) { - return ["Option[", this.scalaType(p.type, true, true), "]"]; - } else { - return this.scalaType(p.type, true); - } - }; - - this.emitDescription(this.descriptionForType(c)); - this.emitLine("case class ", className, " ("); - this.indent(() => { - let count = c.getProperties().size; - let first = true; - this.forEachClassProperty(c, "none", (_, jsonName, p) => { - const nullable = p.type.kind === "union" && nullableFromUnion(p.type as UnionType) !== null; - const nullableOrOptional = p.isOptional || p.type.kind === "null" || nullable; - const last = --count === 0; - const meta: Array<() => void> = []; - - const description = this.descriptionForClassProperty(c, jsonName); - if (description !== undefined) { - meta.push(() => this.emitDescription(description)); - } - - if (meta.length > 0 && !first) { - this.ensureBlankLine(); - } - - for (const emit of meta) { - emit(); - } - - const nameNeedsBackticks = jsonName.endsWith("_") || shouldAddBacktick(jsonName); - const nameWithBackticks = nameNeedsBackticks ? "`" + jsonName + "`" : jsonName; - this.emitLine( - "val ", - nameWithBackticks, - " : ", - scalaType(p), - p.isOptional ? " = None" : nullableOrOptional ? " = None" : "", - last ? "" : "," - ); - - if (meta.length > 0 && !last) { - this.ensureBlankLine(); - } - - first = false; - }); - }); - - this.emitClassDefinitionMethods(); - } - - protected emitClassDefinitionMethods(): void { - this.emitLine(")"); - } - - protected emitEnumDefinition(e: EnumType, enumName: Name): void { - this.emitDescription(this.descriptionForType(e)); - - this.emitBlock( - ["enum ", enumName, " : "], - () => { - let count = e.cases.size; - if (count > 0) { - this.emitItem("\t case "); - } - - this.forEachEnumCase(e, "none", (name, jsonName) => { - if (!(jsonName == "")) { - const backticks = - shouldAddBacktick(jsonName) || - jsonName.includes(" ") || - !isNaN(parseInt(jsonName.charAt(0))); - if (backticks) { - this.emitItem("`"); - } - - this.emitItemOnce([name]); - if (backticks) { - this.emitItem("`"); - } - - if (--count > 0) this.emitItem([","]); - } else { - --count; - } - }); - }, - "none" - ); - } - - protected emitUnionDefinition(u: UnionType, unionName: Name): void { - function sortBy(t: Type): string { - const kind = t.kind; - if (kind === "class") return kind; - return "_" + kind; - } - - this.emitDescription(this.descriptionForType(u)); - - const [maybeNull, nonNulls] = removeNullFromUnion(u, sortBy); - const theTypes: Sourcelike[] = []; - this.forEachUnionMember(u, nonNulls, "none", null, (_, t) => { - theTypes.push(this.scalaType(t)); - }); - if (maybeNull !== null) { - theTypes.push(this.nameForUnionMember(u, maybeNull)); - } - - this.emitItem(["type ", unionName, " = "]); - theTypes.forEach((t, i) => { - this.emitItem(i === 0 ? t : [" | ", t]); - }); - this.ensureBlankLine(); - } - - protected emitSourceStructure(): void { - this.emitHeader(); - - // Top-level arrays, maps - this.forEachTopLevel("leading", (t, name) => { - if (t instanceof ArrayType) { - this.emitTopLevelArray(t, name); - } else if (t instanceof MapType) { - this.emitTopLevelMap(t, name); - } - }); - - this.forEachNamedType( - "leading-and-interposing", - (c: ClassType, n: Name) => this.emitClassDefinition(c, n), - (e, n) => this.emitEnumDefinition(e, n), - (u, n) => this.emitUnionDefinition(u, n) - ); - } -} - -export class UpickleRenderer extends Scala3Renderer { - protected emitClassDefinitionMethods(): void { - this.emitLine(") derives ReadWriter "); - } - - protected emitHeader(): void { - super.emitHeader(); - - this.emitLine("import upickle.default.*"); - this.ensureBlankLine(); - } -} - -export class CirceRenderer extends Scala3Renderer { - private seenUnionTypes: string[] = []; - - protected circeEncoderForType(t: Type, __ = false, noOptional = false, paramName: string = ""): Sourcelike { - return matchType( - t, - _anyType => ["Encoder.encodeJson(", paramName, ")"], - _nullType => ["Encoder.encodeNone(", paramName, ")"], - _boolType => ["Encoder.encodeBoolean(", paramName, ")"], - _integerType => ["Encoder.encodeLong(", paramName, ")"], - _doubleType => ["Encoder.encodeDouble(", paramName, ")"], - _stringType => ["Encoder.encodeString(", paramName, ")"], - arrayType => ["Encoder.encodeSeq[", this.scalaType(arrayType.items), "].apply(", paramName, ")"], - classType => ["Encoder.AsObject[", this.scalaType(classType), "].apply(", paramName, ")"], - mapType => ["Encoder.encodeMap[String,", this.scalaType(mapType.values), "].apply(", paramName, ")"], - _ => ["Encoder.encodeString(", paramName, ")"], - unionType => { - const nullable = nullableFromUnion(unionType); - if (nullable !== null) { - if (noOptional) { - return ["Encoder.AsObject[", this.nameForNamedType(nullable), "]"]; - } else { - return ["Encoder.AsObject[Option[", this.nameForNamedType(nullable), "]]"]; - } - } - - return ["Encoder.AsObject[", this.nameForNamedType(unionType), "]"]; - } - ); - } - - protected emitEmptyClassDefinition(c: ClassType, className: Name): void { - this.emitDescription(this.descriptionForType(c)); - this.ensureBlankLine(); - this.emitLine("case class ", className, "() derives Encoder.AsObject, Decoder"); - } - - protected anySourceType(optional: boolean): Sourcelike { - return [wrapOption("Json", optional)]; - } - - protected emitClassDefinitionMethods(): void { - this.emitLine(") derives Encoder.AsObject, Decoder"); - } - - protected emitEnumDefinition(e: EnumType, enumName: Name): void { - this.emitDescription(this.descriptionForType(e)); - - this.ensureBlankLine(); - this.emitItem(["type ", enumName, " = "]); - let count = e.cases.size; - this.forEachEnumCase(e, "none", (_, jsonName) => { - // if (!(jsonName == "")) { - /* const backticks = - shouldAddBacktick(jsonName) || - jsonName.includes(" ") || - !isNaN(parseInt(jsonName.charAt(0))) - if (backticks) {this.emitItem("`")} else */ - this.emitItem(['"', jsonName, '"']); - // if (backticks) {this.emitItem("`")} - if (--count > 0) this.emitItem([" | "]); - // } else { - // --count - // } - }); - this.ensureBlankLine(); - } - - protected emitHeader(): void { - super.emitHeader(); - - this.emitLine("import scala.util.Try"); - this.emitLine("import io.circe.syntax._"); - this.emitLine("import io.circe._"); - this.emitLine("import cats.syntax.functor._"); - this.ensureBlankLine(); - - this.emitLine("// For serialising string unions"); - this.emitLine( - "given [A <: Singleton](using A <:< String): Decoder[A] = Decoder.decodeString.emapTry(x => Try(x.asInstanceOf[A])) " - ); - this.emitLine( - "given [A <: Singleton](using ev: A <:< String): Encoder[A] = Encoder.encodeString.contramap(ev) " - ); - this.ensureBlankLine(); - this.emitLine("// If a union has a null in, then we'll need this too... "); - this.emitLine("type NullValue = None.type"); - } - - protected emitTopLevelArray(t: ArrayType, name: Name): void { - super.emitTopLevelArray(t, name); - const elementType = this.scalaType(t.items); - this.emitLine([ - "given (using ev : ", - elementType, - "): Encoder[Map[String,", - elementType, - "]] = Encoder.encodeMap[String, ", - elementType, - "]" - ]); - } - - protected emitTopLevelMap(t: MapType, name: Name): void { - super.emitTopLevelMap(t, name); - const elementType = this.scalaType(t.values); - this.ensureBlankLine(); - this.emitLine([ - "given (using ev : ", - elementType, - "): Encoder[Map[String, ", - elementType, - "]] = Encoder.encodeMap[String, ", - elementType, - "]" - ]); - } - - protected emitUnionDefinition(u: UnionType, unionName: Name): void { - function sortBy(t: Type): string { - const kind = t.kind; - if (kind === "class") return kind; - return "_" + kind; - } - - this.emitDescription(this.descriptionForType(u)); - - const [maybeNull, nonNulls] = removeNullFromUnion(u, sortBy); - const theTypes: Sourcelike[] = []; - this.forEachUnionMember(u, nonNulls, "none", null, (_, t) => { - theTypes.push(this.scalaType(t)); - }); - if (maybeNull !== null) { - theTypes.push(this.nameForUnionMember(u, maybeNull)); - } - - this.emitItem(["type ", unionName, " = "]); - theTypes.forEach((t, i) => { - this.emitItem(i === 0 ? t : [" | ", t]); - }); - const thisUnionType = theTypes.map(x => this.sourcelikeToString(x)).join(" | "); - - this.ensureBlankLine(); - if (!this.seenUnionTypes.some(y => y === thisUnionType)) { - this.seenUnionTypes.push(thisUnionType); - const sourceLikeTypes: Array<[Sourcelike, Type]> = []; - this.forEachUnionMember(u, nonNulls, "none", null, (_, t) => { - sourceLikeTypes.push([this.scalaType(t), t]); - }); - if (maybeNull !== null) { - sourceLikeTypes.push([this.nameForUnionMember(u, maybeNull), maybeNull]); - } - - this.emitLine(["given Decoder[", unionName, "] = {"]); - this.indent(() => { - this.emitLine(["List[Decoder[", unionName, "]]("]); - this.indent(() => { - sourceLikeTypes.forEach(t => { - this.emitLine(["Decoder[", t[0], "].widen,"]); - }); - }); - this.emitLine(").reduceLeft(_ or _)"); - }); - this.emitLine(["}"]); - - this.ensureBlankLine(); - - this.emitLine(["given Encoder[", unionName, "] = Encoder.instance {"]); - this.indent(() => { - sourceLikeTypes.forEach((t, i) => { - const paramTemp = `enc${i.toString()}`; - this.emitLine([ - "case ", - paramTemp, - " : ", - t[0], - " => ", - this.circeEncoderForType(t[1], false, false, paramTemp) - ]); - }); - }); - this.emitLine("}"); - } - } -} - -export class Scala3TargetLanguage extends TargetLanguage { - public constructor() { - super("Scala3", ["scala3"], "scala"); - } - - protected getOptions(): Array> { - return [scala3Options.framework, scala3Options.packageName]; - } - - public get supportsOptionalClassProperties(): boolean { - return true; - } - - public get supportsUnionsWithBothNumberTypes(): boolean { - return true; - } - - protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): ConvenienceRenderer { - const options = getOptionValues(scala3Options, untypedOptionValues); - - switch (options.framework) { - case Framework.None: - return new Scala3Renderer(this, renderContext, options); - case Framework.Upickle: - return new UpickleRenderer(this, renderContext, options); - case Framework.Circe: - return new CirceRenderer(this, renderContext, options); - default: - return assertNever(options.framework); - } - } -} diff --git a/packages/quicktype-core/src/language/Scala3/CirceRenderer.ts b/packages/quicktype-core/src/language/Scala3/CirceRenderer.ts new file mode 100644 index 000000000..8be6e660d --- /dev/null +++ b/packages/quicktype-core/src/language/Scala3/CirceRenderer.ts @@ -0,0 +1,193 @@ +import { type Name } from "../../Naming"; +import { type Sourcelike } from "../../Source"; +import { type ArrayType, type ClassType, type EnumType, type MapType, type Type, type UnionType } from "../../Type"; +import { matchType, nullableFromUnion, removeNullFromUnion } from "../../TypeUtils"; + +import { Scala3Renderer } from "./Scala3Renderer"; +import { wrapOption } from "./utils"; + +export class CirceRenderer extends Scala3Renderer { + private seenUnionTypes: string[] = []; + + protected circeEncoderForType(t: Type, __ = false, noOptional = false, paramName: string = ""): Sourcelike { + return matchType( + t, + _anyType => ["Encoder.encodeJson(", paramName, ")"], + _nullType => ["Encoder.encodeNone(", paramName, ")"], + _boolType => ["Encoder.encodeBoolean(", paramName, ")"], + _integerType => ["Encoder.encodeLong(", paramName, ")"], + _doubleType => ["Encoder.encodeDouble(", paramName, ")"], + _stringType => ["Encoder.encodeString(", paramName, ")"], + arrayType => ["Encoder.encodeSeq[", this.scalaType(arrayType.items), "].apply(", paramName, ")"], + classType => ["Encoder.AsObject[", this.scalaType(classType), "].apply(", paramName, ")"], + mapType => ["Encoder.encodeMap[String,", this.scalaType(mapType.values), "].apply(", paramName, ")"], + _ => ["Encoder.encodeString(", paramName, ")"], + unionType => { + const nullable = nullableFromUnion(unionType); + if (nullable !== null) { + if (noOptional) { + return ["Encoder.AsObject[", this.nameForNamedType(nullable), "]"]; + } else { + return ["Encoder.AsObject[Option[", this.nameForNamedType(nullable), "]]"]; + } + } + + return ["Encoder.AsObject[", this.nameForNamedType(unionType), "]"]; + } + ); + } + + protected emitEmptyClassDefinition(c: ClassType, className: Name): void { + this.emitDescription(this.descriptionForType(c)); + this.ensureBlankLine(); + this.emitLine("case class ", className, "() derives Encoder.AsObject, Decoder"); + } + + protected anySourceType(optional: boolean): Sourcelike { + return [wrapOption("Json", optional)]; + } + + protected emitClassDefinitionMethods(): void { + this.emitLine(") derives Encoder.AsObject, Decoder"); + } + + protected emitEnumDefinition(e: EnumType, enumName: Name): void { + this.emitDescription(this.descriptionForType(e)); + + this.ensureBlankLine(); + this.emitItem(["type ", enumName, " = "]); + let count = e.cases.size; + this.forEachEnumCase(e, "none", (_, jsonName) => { + // if (!(jsonName == "")) { + /* const backticks = + shouldAddBacktick(jsonName) || + jsonName.includes(" ") || + !isNaN(parseInt(jsonName.charAt(0))) + if (backticks) {this.emitItem("`")} else */ + this.emitItem(['"', jsonName, '"']); + // if (backticks) {this.emitItem("`")} + if (--count > 0) this.emitItem([" | "]); + // } else { + // --count + // } + }); + this.ensureBlankLine(); + } + + protected emitHeader(): void { + super.emitHeader(); + + this.emitLine("import scala.util.Try"); + this.emitLine("import io.circe.syntax._"); + this.emitLine("import io.circe._"); + this.emitLine("import cats.syntax.functor._"); + this.ensureBlankLine(); + + this.emitLine("// For serialising string unions"); + this.emitLine( + "given [A <: Singleton](using A <:< String): Decoder[A] = Decoder.decodeString.emapTry(x => Try(x.asInstanceOf[A])) " + ); + this.emitLine( + "given [A <: Singleton](using ev: A <:< String): Encoder[A] = Encoder.encodeString.contramap(ev) " + ); + this.ensureBlankLine(); + this.emitLine("// If a union has a null in, then we'll need this too... "); + this.emitLine("type NullValue = None.type"); + } + + protected emitTopLevelArray(t: ArrayType, name: Name): void { + super.emitTopLevelArray(t, name); + const elementType = this.scalaType(t.items); + this.emitLine([ + "given (using ev : ", + elementType, + "): Encoder[Map[String,", + elementType, + "]] = Encoder.encodeMap[String, ", + elementType, + "]" + ]); + } + + protected emitTopLevelMap(t: MapType, name: Name): void { + super.emitTopLevelMap(t, name); + const elementType = this.scalaType(t.values); + this.ensureBlankLine(); + this.emitLine([ + "given (using ev : ", + elementType, + "): Encoder[Map[String, ", + elementType, + "]] = Encoder.encodeMap[String, ", + elementType, + "]" + ]); + } + + protected emitUnionDefinition(u: UnionType, unionName: Name): void { + function sortBy(t: Type): string { + const kind = t.kind; + if (kind === "class") return kind; + return "_" + kind; + } + + this.emitDescription(this.descriptionForType(u)); + + const [maybeNull, nonNulls] = removeNullFromUnion(u, sortBy); + const theTypes: Sourcelike[] = []; + this.forEachUnionMember(u, nonNulls, "none", null, (_, t) => { + theTypes.push(this.scalaType(t)); + }); + if (maybeNull !== null) { + theTypes.push(this.nameForUnionMember(u, maybeNull)); + } + + this.emitItem(["type ", unionName, " = "]); + theTypes.forEach((t, i) => { + this.emitItem(i === 0 ? t : [" | ", t]); + }); + const thisUnionType = theTypes.map(x => this.sourcelikeToString(x)).join(" | "); + + this.ensureBlankLine(); + if (!this.seenUnionTypes.some(y => y === thisUnionType)) { + this.seenUnionTypes.push(thisUnionType); + const sourceLikeTypes: Array<[Sourcelike, Type]> = []; + this.forEachUnionMember(u, nonNulls, "none", null, (_, t) => { + sourceLikeTypes.push([this.scalaType(t), t]); + }); + if (maybeNull !== null) { + sourceLikeTypes.push([this.nameForUnionMember(u, maybeNull), maybeNull]); + } + + this.emitLine(["given Decoder[", unionName, "] = {"]); + this.indent(() => { + this.emitLine(["List[Decoder[", unionName, "]]("]); + this.indent(() => { + sourceLikeTypes.forEach(t => { + this.emitLine(["Decoder[", t[0], "].widen,"]); + }); + }); + this.emitLine(").reduceLeft(_ or _)"); + }); + this.emitLine(["}"]); + + this.ensureBlankLine(); + + this.emitLine(["given Encoder[", unionName, "] = Encoder.instance {"]); + this.indent(() => { + sourceLikeTypes.forEach((t, i) => { + const paramTemp = `enc${i.toString()}`; + this.emitLine([ + "case ", + paramTemp, + " : ", + t[0], + " => ", + this.circeEncoderForType(t[1], false, false, paramTemp) + ]); + }); + }); + this.emitLine("}"); + } + } +} diff --git a/packages/quicktype-core/src/language/Scala3/Scala3Renderer.ts b/packages/quicktype-core/src/language/Scala3/Scala3Renderer.ts new file mode 100644 index 000000000..bc5fe7a56 --- /dev/null +++ b/packages/quicktype-core/src/language/Scala3/Scala3Renderer.ts @@ -0,0 +1,313 @@ +import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../../Annotation"; +import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../../ConvenienceRenderer"; +import { type Name, type Namer, funPrefixNamer } from "../../Naming"; +import { type RenderContext } from "../../Renderer"; +import { type OptionValues } from "../../RendererOptions"; +import { type Sourcelike, maybeAnnotated } from "../../Source"; +import { type TargetLanguage } from "../../TargetLanguage"; +import { + ArrayType, + type ClassProperty, + type ClassType, + type EnumType, + MapType, + type ObjectType, + type Type, + type UnionType +} from "../../Type"; +import { matchType, nullableFromUnion, removeNullFromUnion } from "../../TypeUtils"; + +import { keywords } from "./constants"; +import { type scala3Options } from "./language"; +import { lowerNamingFunction, scalaNameStyle, shouldAddBacktick, upperNamingFunction, wrapOption } from "./utils"; + +export class Scala3Renderer extends ConvenienceRenderer { + public constructor( + targetLanguage: TargetLanguage, + renderContext: RenderContext, + protected readonly _scalaOptions: OptionValues + ) { + super(targetLanguage, renderContext); + } + + protected forbiddenNamesForGlobalNamespace(): readonly string[] { + return keywords; + } + + protected forbiddenForObjectProperties(_: ObjectType, _classNamed: Name): ForbiddenWordsInfo { + return { names: [], includeGlobalForbidden: true }; + } + + protected forbiddenForEnumCases(_: EnumType, _enumName: Name): ForbiddenWordsInfo { + return { names: [], includeGlobalForbidden: true }; + } + + protected forbiddenForUnionMembers(_u: UnionType, _unionName: Name): ForbiddenWordsInfo { + return { names: [], includeGlobalForbidden: false }; + } + + protected topLevelNameStyle(rawName: string): string { + return scalaNameStyle(true, rawName); + } + + protected makeNamedTypeNamer(): Namer { + return upperNamingFunction; + } + + protected namerForObjectProperty(): Namer { + return lowerNamingFunction; + } + + protected makeUnionMemberNamer(): Namer { + return funPrefixNamer("upper", s => scalaNameStyle(true, s) + "Value"); + } + + protected makeEnumCaseNamer(): Namer { + return funPrefixNamer("upper", s => s.replace(" ", "")); // TODO - add backticks where appropriate + } + + protected emitDescriptionBlock(lines: Sourcelike[]): void { + this.emitCommentLines(lines, { lineStart: " * ", beforeComment: "/**", afterComment: " */" }); + } + + protected emitBlock( + line: Sourcelike, + f: () => void, + delimiter: "curly" | "paren" | "lambda" | "none" = "curly" + ): void { + const [open, close] = + delimiter === "curly" + ? ["{", "}"] + : delimiter === "paren" + ? ["(", ")"] + : delimiter === "none" + ? ["", ""] + : ["{", "})"]; + this.emitLine(line, " ", open); + this.indent(f); + this.emitLine(close); + } + + protected anySourceType(optional: boolean): Sourcelike { + return [wrapOption("Any", optional)]; + } + + // (asarazan): I've broken out the following two functions + // because some renderers, such as kotlinx, can cope with `any`, while some get mad. + protected arrayType(arrayType: ArrayType, withIssues = false): Sourcelike { + return ["Seq[", this.scalaType(arrayType.items, withIssues), "]"]; + } + + protected mapType(mapType: MapType, withIssues = false): Sourcelike { + return ["Map[String, ", this.scalaType(mapType.values, withIssues), "]"]; + } + + protected scalaType(t: Type, withIssues = false, noOptional = false): Sourcelike { + return matchType( + t, + _anyType => { + return maybeAnnotated(withIssues, anyTypeIssueAnnotation, this.anySourceType(!noOptional)); + }, + _nullType => { + // return "None.type" + return maybeAnnotated(withIssues, nullTypeIssueAnnotation, this.anySourceType(!noOptional)); + }, + _boolType => "Boolean", + _integerType => "Long", + _doubleType => "Double", + _stringType => "String", + arrayType => this.arrayType(arrayType, withIssues), + classType => this.nameForNamedType(classType), + mapType => this.mapType(mapType, withIssues), + enumType => this.nameForNamedType(enumType), + unionType => { + const nullable = nullableFromUnion(unionType); + if (nullable !== null) { + if (noOptional) { + return [this.scalaType(nullable, withIssues)]; + } else { + return ["Option[", this.scalaType(nullable, withIssues), "]"]; + } + } + + return this.nameForNamedType(unionType); + } + ); + } + + protected emitUsageHeader(): void { + // To be overridden + } + + protected emitHeader(): void { + if (this.leadingComments !== undefined) { + this.emitComments(this.leadingComments); + } else { + this.emitUsageHeader(); + } + + this.ensureBlankLine(); + this.emitLine("package ", this._scalaOptions.packageName); + this.ensureBlankLine(); + } + + protected emitTopLevelArray(t: ArrayType, name: Name): void { + const elementType = this.scalaType(t.items); + this.emitLine(["type ", name, " = List[", elementType, "]"]); + } + + protected emitTopLevelMap(t: MapType, name: Name): void { + const elementType = this.scalaType(t.values); + this.emitLine(["type ", name, " = Map[String, ", elementType, "]"]); + } + + protected emitEmptyClassDefinition(c: ClassType, className: Name): void { + this.emitDescription(this.descriptionForType(c)); + this.emitLine("case class ", className, "()"); + } + + protected emitClassDefinition(c: ClassType, className: Name): void { + if (c.getProperties().size === 0) { + this.emitEmptyClassDefinition(c, className); + return; + } + + const scalaType = (p: ClassProperty): Sourcelike => { + if (p.isOptional) { + return ["Option[", this.scalaType(p.type, true, true), "]"]; + } else { + return this.scalaType(p.type, true); + } + }; + + this.emitDescription(this.descriptionForType(c)); + this.emitLine("case class ", className, " ("); + this.indent(() => { + let count = c.getProperties().size; + let first = true; + this.forEachClassProperty(c, "none", (_, jsonName, p) => { + const nullable = p.type.kind === "union" && nullableFromUnion(p.type as UnionType) !== null; + const nullableOrOptional = p.isOptional || p.type.kind === "null" || nullable; + const last = --count === 0; + const meta: Array<() => void> = []; + + const description = this.descriptionForClassProperty(c, jsonName); + if (description !== undefined) { + meta.push(() => this.emitDescription(description)); + } + + if (meta.length > 0 && !first) { + this.ensureBlankLine(); + } + + for (const emit of meta) { + emit(); + } + + const nameNeedsBackticks = jsonName.endsWith("_") || shouldAddBacktick(jsonName); + const nameWithBackticks = nameNeedsBackticks ? "`" + jsonName + "`" : jsonName; + this.emitLine( + "val ", + nameWithBackticks, + " : ", + scalaType(p), + p.isOptional ? " = None" : nullableOrOptional ? " = None" : "", + last ? "" : "," + ); + + if (meta.length > 0 && !last) { + this.ensureBlankLine(); + } + + first = false; + }); + }); + + this.emitClassDefinitionMethods(); + } + + protected emitClassDefinitionMethods(): void { + this.emitLine(")"); + } + + protected emitEnumDefinition(e: EnumType, enumName: Name): void { + this.emitDescription(this.descriptionForType(e)); + + this.emitBlock( + ["enum ", enumName, " : "], + () => { + let count = e.cases.size; + if (count > 0) { + this.emitItem("\t case "); + } + + this.forEachEnumCase(e, "none", (name, jsonName) => { + if (!(jsonName == "")) { + const backticks = + shouldAddBacktick(jsonName) || + jsonName.includes(" ") || + !isNaN(parseInt(jsonName.charAt(0))); + if (backticks) { + this.emitItem("`"); + } + + this.emitItemOnce([name]); + if (backticks) { + this.emitItem("`"); + } + + if (--count > 0) this.emitItem([","]); + } else { + --count; + } + }); + }, + "none" + ); + } + + protected emitUnionDefinition(u: UnionType, unionName: Name): void { + function sortBy(t: Type): string { + const kind = t.kind; + if (kind === "class") return kind; + return "_" + kind; + } + + this.emitDescription(this.descriptionForType(u)); + + const [maybeNull, nonNulls] = removeNullFromUnion(u, sortBy); + const theTypes: Sourcelike[] = []; + this.forEachUnionMember(u, nonNulls, "none", null, (_, t) => { + theTypes.push(this.scalaType(t)); + }); + if (maybeNull !== null) { + theTypes.push(this.nameForUnionMember(u, maybeNull)); + } + + this.emitItem(["type ", unionName, " = "]); + theTypes.forEach((t, i) => { + this.emitItem(i === 0 ? t : [" | ", t]); + }); + this.ensureBlankLine(); + } + + protected emitSourceStructure(): void { + this.emitHeader(); + + // Top-level arrays, maps + this.forEachTopLevel("leading", (t, name) => { + if (t instanceof ArrayType) { + this.emitTopLevelArray(t, name); + } else if (t instanceof MapType) { + this.emitTopLevelMap(t, name); + } + }); + + this.forEachNamedType( + "leading-and-interposing", + (c: ClassType, n: Name) => this.emitClassDefinition(c, n), + (e, n) => this.emitEnumDefinition(e, n), + (u, n) => this.emitUnionDefinition(u, n) + ); + } +} diff --git a/packages/quicktype-core/src/language/Scala3/UpickleRenderer.ts b/packages/quicktype-core/src/language/Scala3/UpickleRenderer.ts new file mode 100644 index 000000000..0eb5ce354 --- /dev/null +++ b/packages/quicktype-core/src/language/Scala3/UpickleRenderer.ts @@ -0,0 +1,14 @@ +import { Scala3Renderer } from "./Scala3Renderer"; + +export class UpickleRenderer extends Scala3Renderer { + protected emitClassDefinitionMethods(): void { + this.emitLine(") derives ReadWriter "); + } + + protected emitHeader(): void { + super.emitHeader(); + + this.emitLine("import upickle.default.*"); + this.ensureBlankLine(); + } +} diff --git a/packages/quicktype-core/src/language/Scala3/constants.ts b/packages/quicktype-core/src/language/Scala3/constants.ts new file mode 100644 index 000000000..b41844e78 --- /dev/null +++ b/packages/quicktype-core/src/language/Scala3/constants.ts @@ -0,0 +1,87 @@ +// Use backticks for param names with symbols +export const invalidSymbols = [ + ":", + "-", + "+", + "!", + "@", + "#", + "$", + "%", + "^", + "&", + "*", + "(", + ")", + ">", + "<", + "/", + ";", + "'", + '"', + "{", + "}", + ":", + "~", + "`", + "." +] as const; + +export const keywords = [ + "abstract", + "case", + "catch", + "class", + "def", + "do", + "else", + "enum", + "extends", + "export", + "false", + "final", + "finally", + "for", + "forSome", + "if", + "implicit", + "import", + "lazy", + "match", + "new", + "null", + "object", + "override", + "package", + "private", + "protected", + "return", + "sealed", + "super", + "this", + "then", + "throw", + "trait", + "try", + "true", + "type", + "val", + "var", + "while", + "with", + "yield", + "Any", + "Boolean", + "Double", + "Float", + "Long", + "Int", + "Short", + "System", + "Byte", + "String", + "Array", + "List", + "Map", + "Enum" +] as const; diff --git a/packages/quicktype-core/src/language/Scala3/index.ts b/packages/quicktype-core/src/language/Scala3/index.ts new file mode 100644 index 000000000..33c5960ae --- /dev/null +++ b/packages/quicktype-core/src/language/Scala3/index.ts @@ -0,0 +1,4 @@ +export { Scala3TargetLanguage, scala3Options } from "./language"; +export { Scala3Renderer } from "./Scala3Renderer"; +export { CirceRenderer } from "./CirceRenderer"; +export { UpickleRenderer } from "./UpickleRenderer"; diff --git a/packages/quicktype-core/src/language/Scala3/language.ts b/packages/quicktype-core/src/language/Scala3/language.ts new file mode 100644 index 000000000..3b00207d0 --- /dev/null +++ b/packages/quicktype-core/src/language/Scala3/language.ts @@ -0,0 +1,63 @@ +import { type ConvenienceRenderer } from "../../ConvenienceRenderer"; +import { type RenderContext } from "../../Renderer"; +import { EnumOption, type Option, StringOption, getOptionValues } from "../../RendererOptions"; +import { assertNever } from "../../support/Support"; +import { TargetLanguage } from "../../TargetLanguage"; +import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../../types"; + +import { CirceRenderer } from "./CirceRenderer"; +import { Scala3Renderer } from "./Scala3Renderer"; +import { UpickleRenderer } from "./UpickleRenderer"; + +export enum Framework { + None = "None", + Upickle = "Upickle", + Circe = "Circe" +} + +export const scala3Options = { + framework: new EnumOption( + "framework", + "Serialization framework", + [ + ["just-types", Framework.None], + ["circe", Framework.Circe], + ["upickle", Framework.Upickle] + ], + undefined + ), + packageName: new StringOption("package", "Package", "PACKAGE", "quicktype") +}; + +export class Scala3TargetLanguage extends TargetLanguage { + public constructor() { + super("Scala3", ["scala3"], "scala"); + } + + protected getOptions(): Array> { + return [scala3Options.framework, scala3Options.packageName]; + } + + public get supportsOptionalClassProperties(): boolean { + return true; + } + + public get supportsUnionsWithBothNumberTypes(): boolean { + return true; + } + + protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): ConvenienceRenderer { + const options = getOptionValues(scala3Options, untypedOptionValues); + + switch (options.framework) { + case Framework.None: + return new Scala3Renderer(this, renderContext, options); + case Framework.Upickle: + return new UpickleRenderer(this, renderContext, options); + case Framework.Circe: + return new CirceRenderer(this, renderContext, options); + default: + return assertNever(options.framework); + } + } +} diff --git a/packages/quicktype-core/src/language/Scala3/utils.ts b/packages/quicktype-core/src/language/Scala3/utils.ts new file mode 100644 index 000000000..1de23bcb8 --- /dev/null +++ b/packages/quicktype-core/src/language/Scala3/utils.ts @@ -0,0 +1,73 @@ +import { funPrefixNamer } from "../../Naming"; +import { + allLowerWordStyle, + allUpperWordStyle, + combineWords, + firstUpperWordStyle, + isDigit, + isLetterOrUnderscore, + isNumeric, + legalizeCharacters, + splitIntoWords +} from "../../support/Strings"; + +import { invalidSymbols, keywords } from "./constants"; + +/** + * Check if given parameter name should be wrapped in a backtick + * @param paramName + */ +export const shouldAddBacktick = (paramName: string): boolean => { + return ( + keywords.some(s => paramName === s) || + invalidSymbols.some(s => paramName.includes(s)) || + !isNaN(+parseFloat(paramName)) || + !isNaN(parseInt(paramName.charAt(0))) + ); +}; + +export const wrapOption = (s: string, optional: boolean): string => { + if (optional) { + return "Option[" + s + "]"; + } else { + return s; + } +}; + +function isPartCharacter(codePoint: number): boolean { + return isLetterOrUnderscore(codePoint) || isNumeric(codePoint); +} + +function isStartCharacter(codePoint: number): boolean { + return isPartCharacter(codePoint) && !isDigit(codePoint); +} + +const legalizeName = legalizeCharacters(isPartCharacter); + +export function scalaNameStyle(isUpper: boolean, original: string): string { + const words = splitIntoWords(original); + return combineWords( + words, + legalizeName, + isUpper ? firstUpperWordStyle : allLowerWordStyle, + firstUpperWordStyle, + isUpper ? allUpperWordStyle : allLowerWordStyle, + allUpperWordStyle, + "", + isStartCharacter + ); +} + +/* function unicodeEscape(codePoint: number): string { + return "\\u" + intToHex(codePoint, 4); +} */ + +// const _stringEscape = utf32ConcatMap(escapeNonPrintableMapper(isPrintable, unicodeEscape)); + +/* function stringEscape(s: string): string { + // "$this" is a template string in Kotlin so we have to escape $ + return _stringEscape(s).replace(/\$/g, "\\$"); +} */ + +export const upperNamingFunction = funPrefixNamer("upper", s => scalaNameStyle(true, s)); +export const lowerNamingFunction = funPrefixNamer("lower", s => scalaNameStyle(false, s)); diff --git a/packages/quicktype-core/src/language/Smithy4s.ts b/packages/quicktype-core/src/language/Smithy4s/Smithy4sRenderer.ts similarity index 73% rename from packages/quicktype-core/src/language/Smithy4s.ts rename to packages/quicktype-core/src/language/Smithy4s/Smithy4sRenderer.ts index 163b33073..17fda57b2 100644 --- a/packages/quicktype-core/src/language/Smithy4s.ts +++ b/packages/quicktype-core/src/language/Smithy4s/Smithy4sRenderer.ts @@ -1,22 +1,10 @@ -import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../Annotation"; -import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../ConvenienceRenderer"; -import { type Name, type Namer, funPrefixNamer } from "../Naming"; -import { type RenderContext } from "../Renderer"; -import { EnumOption, type Option, type OptionValues, StringOption, getOptionValues } from "../RendererOptions"; -import { type Sourcelike, maybeAnnotated } from "../Source"; -import { - allLowerWordStyle, - allUpperWordStyle, - combineWords, - firstUpperWordStyle, - isDigit, - isLetterOrUnderscore, - isNumeric, - legalizeCharacters, - splitIntoWords -} from "../support/Strings"; -import { assertNever } from "../support/Support"; -import { TargetLanguage } from "../TargetLanguage"; +import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../../Annotation"; +import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../../ConvenienceRenderer"; +import { type Name, type Namer, funPrefixNamer } from "../../Naming"; +import { type RenderContext } from "../../Renderer"; +import { type OptionValues } from "../../RendererOptions"; +import { type Sourcelike, maybeAnnotated } from "../../Source"; +import { type TargetLanguage } from "../../TargetLanguage"; import { ArrayType, type ClassProperty, @@ -26,147 +14,23 @@ import { type ObjectType, type Type, type UnionType -} from "../Type"; -import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../types"; -import { matchCompoundType, matchType, nullableFromUnion, removeNullFromUnion } from "../TypeUtils"; - -export enum Framework { - None = "None" -} +} from "../../Type"; +import { matchCompoundType, matchType, nullableFromUnion, removeNullFromUnion } from "../../TypeUtils"; -export const SmithyOptions = { - framework: new EnumOption("framework", "Serialization framework", [["just-types", Framework.None]], undefined), - packageName: new StringOption("package", "Package", "PACKAGE", "quicktype") -}; - -// Use backticks for param names with symbols -const invalidSymbols = [ - ":", - "-", - "+", - "!", - "@", - "#", - "%", - "^", - "&", - "*", - "(", - ")", - ">", - "<", - "/", - ";", - "'", - '"', - "{", - "}", - ":", - "~", - "`", - "." -]; - -const keywords = [ - "abstract", - "case", - "catch", - "do", - "else", - "export", - "false", - "final", - "finally", - "for", - "forSome", - "if", - "implicit", - "import", - "new", - "override", - "package", - "private", - "protected", - "return", - "sealed", - "super", - "this", - "then", - "throw", - "trait", - "try", - "true", - "val", - "var", - "while", - "with", - "yield", - "Any", - "Boolean", - "Double", - "Float", - "Long", - "Int", - "Short", - "System", - "Byte", - "String", - "Array", - "List", - "Map", - "Enum" -]; - -/** - * Check if given parameter name should be wrapped in a backtick - * @param paramName - */ -const shouldAddBacktick = (paramName: string): boolean => { - return ( - keywords.some(s => paramName === s) || - invalidSymbols.some(s => paramName.includes(s)) || - !isNaN(parseFloat(paramName)) || - !isNaN(parseInt(paramName.charAt(0))) - ); -}; - -function isPartCharacter(codePoint: number): boolean { - return isLetterOrUnderscore(codePoint) || isNumeric(codePoint); -} - -function isStartCharacter(codePoint: number): boolean { - return isPartCharacter(codePoint) && !isDigit(codePoint); -} - -const legalizeName = legalizeCharacters(isPartCharacter); - -function scalaNameStyle(isUpper: boolean, original: string): string { - const words = splitIntoWords(original); - return combineWords( - words, - legalizeName, - isUpper ? firstUpperWordStyle : allLowerWordStyle, - firstUpperWordStyle, - isUpper ? allUpperWordStyle : allLowerWordStyle, - allUpperWordStyle, - "", - isStartCharacter - ); -} - -const upperNamingFunction = funPrefixNamer("upper", s => scalaNameStyle(true, s)); -const lowerNamingFunction = funPrefixNamer("lower", s => scalaNameStyle(false, s)); +import { keywords } from "./constants"; +import { type smithyOptions } from "./language"; +import { lowerNamingFunction, scalaNameStyle, shouldAddBacktick, upperNamingFunction } from "./utils"; export class Smithy4sRenderer extends ConvenienceRenderer { public constructor( targetLanguage: TargetLanguage, renderContext: RenderContext, - protected readonly _scalaOptions: OptionValues + protected readonly _scalaOptions: OptionValues ) { super(targetLanguage, renderContext); } - protected forbiddenNamesForGlobalNamespace(): string[] { + protected forbiddenNamesForGlobalNamespace(): readonly string[] { return keywords; } @@ -419,10 +283,10 @@ export class Smithy4sRenderer extends ConvenienceRenderer { this.forEachEnumCase(e, "none", (name, jsonName) => { // if (!(jsonName == "")) { /* const backticks = - shouldAddBacktick(jsonName) || - jsonName.includes(" ") || - !isNaN(parseInt(jsonName.charAt(0))) - if (backticks) {this.emitItem("`")} else */ + shouldAddBacktick(jsonName) || + jsonName.includes(" ") || + !isNaN(parseInt(jsonName.charAt(0))) + if (backticks) {this.emitItem("`")} else */ this.emitLine(); this.emitItem([name, ' = "', jsonName, '"']); @@ -524,32 +388,3 @@ export class Smithy4sRenderer extends ConvenienceRenderer { ); } } - -export class SmithyTargetLanguage extends TargetLanguage { - public constructor() { - super("Smithy", ["Smithy"], "smithy"); - } - - protected getOptions(): Array> { - return [SmithyOptions.framework, SmithyOptions.packageName]; - } - - public get supportsOptionalClassProperties(): boolean { - return true; - } - - public get supportsUnionsWithBothNumberTypes(): boolean { - return true; - } - - protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): ConvenienceRenderer { - const options = getOptionValues(SmithyOptions, untypedOptionValues); - - switch (options.framework) { - case Framework.None: - return new Smithy4sRenderer(this, renderContext, options); - default: - return assertNever(options.framework); - } - } -} diff --git a/packages/quicktype-core/src/language/Smithy4s/constants.ts b/packages/quicktype-core/src/language/Smithy4s/constants.ts new file mode 100644 index 000000000..e015b6ea8 --- /dev/null +++ b/packages/quicktype-core/src/language/Smithy4s/constants.ts @@ -0,0 +1,77 @@ +// Use backticks for param names with symbols +export const invalidSymbols = [ + ":", + "-", + "+", + "!", + "@", + "#", + "%", + "^", + "&", + "*", + "(", + ")", + ">", + "<", + "/", + ";", + "'", + '"', + "{", + "}", + ":", + "~", + "`", + "." +] as const; + +export const keywords = [ + "abstract", + "case", + "catch", + "do", + "else", + "export", + "false", + "final", + "finally", + "for", + "forSome", + "if", + "implicit", + "import", + "new", + "override", + "package", + "private", + "protected", + "return", + "sealed", + "super", + "this", + "then", + "throw", + "trait", + "try", + "true", + "val", + "var", + "while", + "with", + "yield", + "Any", + "Boolean", + "Double", + "Float", + "Long", + "Int", + "Short", + "System", + "Byte", + "String", + "Array", + "List", + "Map", + "Enum" +] as const; diff --git a/packages/quicktype-core/src/language/Smithy4s/index.ts b/packages/quicktype-core/src/language/Smithy4s/index.ts new file mode 100644 index 000000000..1dea97f90 --- /dev/null +++ b/packages/quicktype-core/src/language/Smithy4s/index.ts @@ -0,0 +1,2 @@ +export { SmithyTargetLanguage, smithyOptions } from "./language"; +export { Smithy4sRenderer } from "./Smithy4sRenderer"; diff --git a/packages/quicktype-core/src/language/Smithy4s/language.ts b/packages/quicktype-core/src/language/Smithy4s/language.ts new file mode 100644 index 000000000..2726d0ff5 --- /dev/null +++ b/packages/quicktype-core/src/language/Smithy4s/language.ts @@ -0,0 +1,46 @@ +import { type ConvenienceRenderer } from "../../ConvenienceRenderer"; +import { type RenderContext } from "../../Renderer"; +import { EnumOption, type Option, StringOption, getOptionValues } from "../../RendererOptions"; +import { assertNever } from "../../support/Support"; +import { TargetLanguage } from "../../TargetLanguage"; +import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../../types"; + +import { Smithy4sRenderer } from "./Smithy4sRenderer"; + +export enum Framework { + None = "None" +} + +export const smithyOptions = { + framework: new EnumOption("framework", "Serialization framework", [["just-types", Framework.None]], undefined), + packageName: new StringOption("package", "Package", "PACKAGE", "quicktype") +}; + +export class SmithyTargetLanguage extends TargetLanguage { + public constructor() { + super("Smithy", ["Smithy"], "smithy"); + } + + protected getOptions(): Array> { + return [smithyOptions.framework, smithyOptions.packageName]; + } + + public get supportsOptionalClassProperties(): boolean { + return true; + } + + public get supportsUnionsWithBothNumberTypes(): boolean { + return true; + } + + protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): ConvenienceRenderer { + const options = getOptionValues(smithyOptions, untypedOptionValues); + + switch (options.framework) { + case Framework.None: + return new Smithy4sRenderer(this, renderContext, options); + default: + return assertNever(options.framework); + } + } +} diff --git a/packages/quicktype-core/src/language/Smithy4s/utils.ts b/packages/quicktype-core/src/language/Smithy4s/utils.ts new file mode 100644 index 000000000..7075f6627 --- /dev/null +++ b/packages/quicktype-core/src/language/Smithy4s/utils.ts @@ -0,0 +1,55 @@ +import { isDigit } from "unicode-properties"; + +import { funPrefixNamer } from "../../Naming"; +import { + allLowerWordStyle, + allUpperWordStyle, + combineWords, + firstUpperWordStyle, + isLetterOrUnderscore, + isNumeric, + legalizeCharacters, + splitIntoWords +} from "../../support/Strings"; + +import { invalidSymbols, keywords } from "./constants"; + +/** + * Check if given parameter name should be wrapped in a backtick + * @param paramName + */ +export const shouldAddBacktick = (paramName: string): boolean => { + return ( + keywords.some(s => paramName === s) || + invalidSymbols.some(s => paramName.includes(s)) || + !isNaN(parseFloat(paramName)) || + !isNaN(parseInt(paramName.charAt(0))) + ); +}; + +function isPartCharacter(codePoint: number): boolean { + return isLetterOrUnderscore(codePoint) || isNumeric(codePoint); +} + +function isStartCharacter(codePoint: number): boolean { + return isPartCharacter(codePoint) && !isDigit(codePoint); +} + +const legalizeName = legalizeCharacters(isPartCharacter); + +export function scalaNameStyle(isUpper: boolean, original: string): string { + const words = splitIntoWords(original); + return combineWords( + words, + legalizeName, + isUpper ? firstUpperWordStyle : allLowerWordStyle, + firstUpperWordStyle, + isUpper ? allUpperWordStyle : allLowerWordStyle, + allUpperWordStyle, + "", + isStartCharacter + ); +} + +export const upperNamingFunction = funPrefixNamer("upper", s => scalaNameStyle(true, s)); +export const lowerNamingFunction = funPrefixNamer("lower", s => scalaNameStyle(false, s)); diff --git a/packages/quicktype-core/src/language/Swift.ts b/packages/quicktype-core/src/language/Swift/SwiftRenderer.ts similarity index 66% rename from packages/quicktype-core/src/language/Swift.ts rename to packages/quicktype-core/src/language/Swift/SwiftRenderer.ts index cb801de2b..18cc7f281 100644 --- a/packages/quicktype-core/src/language/Swift.ts +++ b/packages/quicktype-core/src/language/Swift/SwiftRenderer.ts @@ -1,337 +1,30 @@ import { arrayIntercalate } from "collection-utils"; -import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../Annotation"; -import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../ConvenienceRenderer"; -import { type DateTimeRecognizer, DefaultDateTimeRecognizer } from "../DateTime"; -import { type Name, type Namer, funPrefixNamer } from "../Naming"; -import { type ForEachPosition, type RenderContext } from "../Renderer"; -import { - BooleanOption, - EnumOption, - type Option, - type OptionValues, - StringOption, - getOptionValues -} from "../RendererOptions"; -import { type Sourcelike, maybeAnnotated, modifySource } from "../Source"; -import { AcronymStyleOptions, acronymOption, acronymStyle } from "../support/Acronyms"; -import { - addPrefixIfNecessary, - allLowerWordStyle, - allUpperWordStyle, - camelCase, - combineWords, - escapeNonPrintableMapper, - firstUpperWordStyle, - intToHex, - isDigit, - isLetterOrUnderscore, - isNumeric, - isPrintable, - legalizeCharacters, - splitIntoWords, - utf32ConcatMap -} from "../support/Strings"; -import { assert, defined, panic } from "../support/Support"; -import { TargetLanguage } from "../TargetLanguage"; +import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../../Annotation"; +import { ConvenienceRenderer, type ForbiddenWordsInfo } from "../../ConvenienceRenderer"; +import { type Name, type Namer, funPrefixNamer } from "../../Naming"; +import { type RenderContext } from "../../Renderer"; +import { type OptionValues } from "../../RendererOptions"; +import { type Sourcelike, maybeAnnotated, modifySource } from "../../Source"; +import { acronymStyle } from "../../support/Acronyms"; +import { camelCase } from "../../support/Strings"; +import { assert, defined, panic } from "../../support/Support"; +import { type TargetLanguage } from "../../TargetLanguage"; import { ArrayType, type ClassProperty, type ClassType, EnumType, MapType, - type PrimitiveStringTypeKind, - type TransformedStringTypeKind, type Type, type TypeKind, type UnionType -} from "../Type"; -import { type StringTypeMapping } from "../TypeBuilder"; -import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../types"; -import { matchType, nullableFromUnion, removeNullFromUnion } from "../TypeUtils"; - -const MAX_SAMELINE_PROPERTIES = 4; - -export const swiftOptions = { - justTypes: new BooleanOption("just-types", "Plain types only", false), - convenienceInitializers: new BooleanOption("initializers", "Generate initializers and mutators", true), - explicitCodingKeys: new BooleanOption("coding-keys", "Explicit CodingKey values in Codable types", true), - codingKeysProtocol: new StringOption( - "coding-keys-protocol", - "CodingKeys implements protocols", - "protocol1, protocol2...", - "", - "secondary" - ), - alamofire: new BooleanOption("alamofire", "Alamofire extensions", false), - namedTypePrefix: new StringOption("type-prefix", "Prefix for type names", "PREFIX", "", "secondary"), - useClasses: new EnumOption("struct-or-class", "Structs or classes", [ - ["struct", false], - ["class", true] - ]), - mutableProperties: new BooleanOption("mutable-properties", "Use var instead of let for object properties", false), - acronymStyle: acronymOption(AcronymStyleOptions.Pascal), - dense: new EnumOption( - "density", - "Code density", - [ - ["dense", true], - ["normal", false] - ], - "dense", - "secondary" - ), - linux: new BooleanOption("support-linux", "Support Linux", false, "secondary"), - objcSupport: new BooleanOption( - "objective-c-support", - "Objects inherit from NSObject and @objcMembers is added to classes", - false - ), - optionalEnums: new BooleanOption("optional-enums", "If no matching case is found enum value is set to null", false), - swift5Support: new BooleanOption("swift-5-support", "Renders output in a Swift 5 compatible mode", false), - sendable: new BooleanOption("sendable", "Mark generated models as Sendable", false), - multiFileOutput: new BooleanOption( - "multi-file-output", - "Renders each top-level object in its own Swift file", - false - ), - accessLevel: new EnumOption( - "access-level", - "Access level", - [ - ["internal", "internal"], - ["public", "public"] - ], - "internal", - "secondary" - ), - protocol: new EnumOption( - "protocol", - "Make types implement protocol", - [ - ["none", { equatable: false, hashable: false }], - ["equatable", { equatable: true, hashable: false }], - ["hashable", { equatable: false, hashable: true }] - ], - "none", - "secondary" - ) -}; - -// These are all recognized by Swift as ISO8601 date-times: -// -// 2018-08-14T02:45:50+00:00 -// 2018-08-14T02:45:50+00 -// 2018-08-14T02:45:50+1 -// 2018-08-14T02:45:50+1111 -// 2018-08-14T02:45:50+1111:1:33 -// 2018-08-14T02:45:50-00 -// 2018-08-14T02:45:50z -// 2018-00008-1T002:45:3Z - -const swiftDateTimeRegex = /^\d+-\d+-\d+T\d+:\d+:\d+([zZ]|[+-]\d+(:\d+)?)$/; - -class SwiftDateTimeRecognizer extends DefaultDateTimeRecognizer { - public isDateTime(str: string): boolean { - return swiftDateTimeRegex.exec(str) !== null; - } -} - -export interface SwiftProperty { - jsonName: string; - name: Name; - parameter: ClassProperty; - position: ForEachPosition; -} - -export class SwiftTargetLanguage extends TargetLanguage { - public constructor() { - super("Swift", ["swift", "swift4"], "swift"); - } - - protected getOptions(): Array> { - return [ - swiftOptions.justTypes, - swiftOptions.useClasses, - swiftOptions.dense, - swiftOptions.convenienceInitializers, - swiftOptions.explicitCodingKeys, - swiftOptions.codingKeysProtocol, - swiftOptions.accessLevel, - swiftOptions.alamofire, - swiftOptions.linux, - swiftOptions.namedTypePrefix, - swiftOptions.protocol, - swiftOptions.acronymStyle, - swiftOptions.objcSupport, - swiftOptions.optionalEnums, - swiftOptions.sendable, - swiftOptions.swift5Support, - swiftOptions.multiFileOutput, - swiftOptions.mutableProperties - ]; - } - - public get stringTypeMapping(): StringTypeMapping { - const mapping: Map = new Map(); - mapping.set("date-time", "date-time"); - return mapping; - } - - public get supportsOptionalClassProperties(): boolean { - return true; - } - - public get supportsUnionsWithBothNumberTypes(): boolean { - return true; - } - - protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): SwiftRenderer { - return new SwiftRenderer(this, renderContext, getOptionValues(swiftOptions, untypedOptionValues)); - } - - public get dateTimeRecognizer(): DateTimeRecognizer { - return new SwiftDateTimeRecognizer(); - } -} +} from "../../Type"; +import { matchType, nullableFromUnion, removeNullFromUnion } from "../../TypeUtils"; -const keywords = [ - "await", - "associatedtype", - "class", - "deinit", - "enum", - "extension", - "fileprivate", - "func", - "import", - "init", - "inout", - "internal", - "let", - "open", - "operator", - "private", - "protocol", - "public", - "static", - "struct", - "subscript", - "typealias", - "var", - "break", - "case", - "continue", - "default", - "defer", - "do", - "else", - "fallthrough", - "for", - "guard", - "if", - "in", - "repeat", - "return", - "switch", - "where", - "while", - "as", - "Any", - "catch", - "false", - "is", - "nil", - "rethrows", - "super", - "self", - "Self", - "throw", - "throws", - "true", - "try", - "_", - "associativity", - "convenience", - "dynamic", - "didSet", - "final", - "get", - "infix", - "indirect", - "lazy", - "left", - "mutating", - "nonmutating", - "optional", - "override", - "postfix", - "precedence", - "prefix", - "Protocol", - "required", - "right", - "set", - "Type", - "unowned", - "weak", - "willSet", - "String", - "Int", - "Double", - "Bool", - "Data", - "Date", - "URL", - "CommandLine", - "FileHandle", - "JSONSerialization", - "checkNull", - "removeNSNull", - "nilToNSNull", - "convertArray", - "convertOptional", - "convertDict", - "convertDouble", - "jsonString", - "jsonData" -]; - -function isPartCharacter(codePoint: number): boolean { - return isLetterOrUnderscore(codePoint) || isNumeric(codePoint); -} - -function isStartCharacter(codePoint: number): boolean { - return isPartCharacter(codePoint) && !isDigit(codePoint); -} - -const legalizeName = legalizeCharacters(isPartCharacter); - -function swiftNameStyle( - prefix: string, - isUpper: boolean, - original: string, - acronymsStyle: (s: string) => string = allUpperWordStyle -): string { - const words = splitIntoWords(original); - const combined = combineWords( - words, - legalizeName, - isUpper ? firstUpperWordStyle : allLowerWordStyle, - firstUpperWordStyle, - isUpper ? allUpperWordStyle : allLowerWordStyle, - acronymsStyle, - "", - isStartCharacter - ); - return addPrefixIfNecessary(prefix, combined); -} - -function unicodeEscape(codePoint: number): string { - return "\\u{" + intToHex(codePoint, 0) + "}"; -} - -const stringEscape = utf32ConcatMap(escapeNonPrintableMapper(isPrintable, unicodeEscape)); +import { keywords } from "./constants"; +import { type swiftOptions } from "./language"; +import { MAX_SAMELINE_PROPERTIES, type SwiftProperty, stringEscape, swiftNameStyle } from "./utils"; export class SwiftRenderer extends ConvenienceRenderer { private _currentFilename: string | undefined; @@ -348,9 +41,9 @@ export class SwiftRenderer extends ConvenienceRenderer { super(targetLanguage, renderContext); } - protected forbiddenNamesForGlobalNamespace(): string[] { + protected forbiddenNamesForGlobalNamespace(): readonly string[] { if (this._options.alamofire) { - return ["DataRequest", ...keywords]; + return ["DataRequest", ...keywords] as const; } return keywords; @@ -859,22 +552,22 @@ export class SwiftRenderer extends ConvenienceRenderer { }); } else { this.emitMultiline(`decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in - let container = try decoder.singleValueContainer() - let dateStr = try container.decode(String.self) - - let formatter = DateFormatter() - formatter.calendar = Calendar(identifier: .iso8601) - formatter.locale = Locale(identifier: "en_US_POSIX") - formatter.timeZone = TimeZone(secondsFromGMT: 0) - formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX" - if let date = formatter.date(from: dateStr) { - return date - } - formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssXXXXX" - if let date = formatter.date(from: dateStr) { - return date - } - throw DecodingError.typeMismatch(Date.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not decode date")) + let container = try decoder.singleValueContainer() + let dateStr = try container.decode(String.self) + + let formatter = DateFormatter() + formatter.calendar = Calendar(identifier: .iso8601) + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeZone = TimeZone(secondsFromGMT: 0) + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX" + if let date = formatter.date(from: dateStr) { + return date + } + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssXXXXX" + if let date = formatter.date(from: dateStr) { + return date + } + throw DecodingError.typeMismatch(Date.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not decode date")) })`); } @@ -1148,20 +841,20 @@ encoder.dateEncodingStrategy = .formatted(formatter)`); this.ensureBlankLine(); this.emitMultiline(` public static func == (lhs: JSONNull, rhs: JSONNull) -> Bool { - return true - }`); + return true + }`); if (this._options.objcSupport === false) { this.ensureBlankLine(); this.emitMultiline(` public var hashValue: Int { - return 0 - }`); + return 0 + }`); if (this._options.swift5Support) { this.ensureBlankLine(); this.emitMultiline(` public func hash(into hasher: inout Hasher) { - // No-op - }`); + // No-op + }`); } } @@ -1173,41 +866,41 @@ encoder.dateEncodingStrategy = .formatted(formatter)`); } this.emitMultiline(`public init() {} - - public required init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - if !container.decodeNil() { - throw DecodingError.typeMismatch(JSONNull.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for JSONNull")) - } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encodeNil() - } + + public required init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if !container.decodeNil() { + throw DecodingError.typeMismatch(JSONNull.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for JSONNull")) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encodeNil() + } }`); } if (this._needAny) { this.ensureBlankLine(); this.emitMultiline(`class JSONCodingKey: CodingKey { - let key: String - - required init?(intValue: Int) { - return nil - } - - required init?(stringValue: String) { - key = stringValue - } - - var intValue: Int? { - return nil - } - - var stringValue: String { - return key - } + let key: String + + required init?(intValue: Int) { + return nil + } + + required init?(stringValue: String) { + key = stringValue + } + + var intValue: Int? { + return nil + } + + var stringValue: String { + return key + } }`); this.ensureBlankLine(); @@ -1219,196 +912,196 @@ encoder.dateEncodingStrategy = .formatted(formatter)`); this.ensureBlankLine(); this.emitMultiline(` ${this.accessLevel}let value: Any - - static func decodingError(forCodingPath codingPath: [CodingKey]) -> DecodingError { - let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode JSONAny") - return DecodingError.typeMismatch(JSONAny.self, context) - } - - static func encodingError(forValue value: Any, codingPath: [CodingKey]) -> EncodingError { - let context = EncodingError.Context(codingPath: codingPath, debugDescription: "Cannot encode JSONAny") - return EncodingError.invalidValue(value, context) - } - - static func decode(from container: SingleValueDecodingContainer) throws -> Any { - if let value = try? container.decode(Bool.self) { - return value - } - if let value = try? container.decode(Int64.self) { - return value - } - if let value = try? container.decode(Double.self) { - return value - } - if let value = try? container.decode(String.self) { - return value - } - if container.decodeNil() { - return JSONNull() - } - throw decodingError(forCodingPath: container.codingPath) - } - - static func decode(from container: inout UnkeyedDecodingContainer) throws -> Any { - if let value = try? container.decode(Bool.self) { - return value - } - if let value = try? container.decode(Int64.self) { - return value - } - if let value = try? container.decode(Double.self) { - return value - } - if let value = try? container.decode(String.self) { - return value - } - if let value = try? container.decodeNil() { - if value { - return JSONNull() - } - } - if var container = try? container.nestedUnkeyedContainer() { - return try decodeArray(from: &container) - } - if var container = try? container.nestedContainer(keyedBy: JSONCodingKey.self) { - return try decodeDictionary(from: &container) - } - throw decodingError(forCodingPath: container.codingPath) - } - - static func decode(from container: inout KeyedDecodingContainer, forKey key: JSONCodingKey) throws -> Any { - if let value = try? container.decode(Bool.self, forKey: key) { - return value - } - if let value = try? container.decode(Int64.self, forKey: key) { - return value - } - if let value = try? container.decode(Double.self, forKey: key) { - return value - } - if let value = try? container.decode(String.self, forKey: key) { - return value - } - if let value = try? container.decodeNil(forKey: key) { - if value { - return JSONNull() - } - } - if var container = try? container.nestedUnkeyedContainer(forKey: key) { - return try decodeArray(from: &container) - } - if var container = try? container.nestedContainer(keyedBy: JSONCodingKey.self, forKey: key) { - return try decodeDictionary(from: &container) - } - throw decodingError(forCodingPath: container.codingPath) - } - - static func decodeArray(from container: inout UnkeyedDecodingContainer) throws -> [Any] { - var arr: [Any] = [] - while !container.isAtEnd { - let value = try decode(from: &container) - arr.append(value) - } - return arr - } - - static func decodeDictionary(from container: inout KeyedDecodingContainer) throws -> [String: Any] { - var dict = [String: Any]() - for key in container.allKeys { - let value = try decode(from: &container, forKey: key) - dict[key.stringValue] = value - } - return dict - } - - static func encode(to container: inout UnkeyedEncodingContainer, array: [Any]) throws { - for value in array { - if let value = value as? Bool { - try container.encode(value) - } else if let value = value as? Int64 { - try container.encode(value) - } else if let value = value as? Double { - try container.encode(value) - } else if let value = value as? String { - try container.encode(value) - } else if value is JSONNull { - try container.encodeNil() - } else if let value = value as? [Any] { - var container = container.nestedUnkeyedContainer() - try encode(to: &container, array: value) - } else if let value = value as? [String: Any] { - var container = container.nestedContainer(keyedBy: JSONCodingKey.self) - try encode(to: &container, dictionary: value) - } else { - throw encodingError(forValue: value, codingPath: container.codingPath) - } - } - } - - static func encode(to container: inout KeyedEncodingContainer, dictionary: [String: Any]) throws { - for (key, value) in dictionary { - let key = JSONCodingKey(stringValue: key)! - if let value = value as? Bool { - try container.encode(value, forKey: key) - } else if let value = value as? Int64 { - try container.encode(value, forKey: key) - } else if let value = value as? Double { - try container.encode(value, forKey: key) - } else if let value = value as? String { - try container.encode(value, forKey: key) - } else if value is JSONNull { - try container.encodeNil(forKey: key) - } else if let value = value as? [Any] { - var container = container.nestedUnkeyedContainer(forKey: key) - try encode(to: &container, array: value) - } else if let value = value as? [String: Any] { - var container = container.nestedContainer(keyedBy: JSONCodingKey.self, forKey: key) - try encode(to: &container, dictionary: value) - } else { - throw encodingError(forValue: value, codingPath: container.codingPath) - } - } - } - - static func encode(to container: inout SingleValueEncodingContainer, value: Any) throws { - if let value = value as? Bool { - try container.encode(value) - } else if let value = value as? Int64 { - try container.encode(value) - } else if let value = value as? Double { - try container.encode(value) - } else if let value = value as? String { - try container.encode(value) - } else if value is JSONNull { - try container.encodeNil() - } else { - throw encodingError(forValue: value, codingPath: container.codingPath) - } - } - - public required init(from decoder: Decoder) throws { - if var arrayContainer = try? decoder.unkeyedContainer() { - self.value = try JSONAny.decodeArray(from: &arrayContainer) - } else if var container = try? decoder.container(keyedBy: JSONCodingKey.self) { - self.value = try JSONAny.decodeDictionary(from: &container) - } else { - let container = try decoder.singleValueContainer() - self.value = try JSONAny.decode(from: container) - } - } - - public func encode(to encoder: Encoder) throws { - if let arr = self.value as? [Any] { - var container = encoder.unkeyedContainer() - try JSONAny.encode(to: &container, array: arr) - } else if let dict = self.value as? [String: Any] { - var container = encoder.container(keyedBy: JSONCodingKey.self) - try JSONAny.encode(to: &container, dictionary: dict) - } else { - var container = encoder.singleValueContainer() - try JSONAny.encode(to: &container, value: self.value) - } - } + + static func decodingError(forCodingPath codingPath: [CodingKey]) -> DecodingError { + let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode JSONAny") + return DecodingError.typeMismatch(JSONAny.self, context) + } + + static func encodingError(forValue value: Any, codingPath: [CodingKey]) -> EncodingError { + let context = EncodingError.Context(codingPath: codingPath, debugDescription: "Cannot encode JSONAny") + return EncodingError.invalidValue(value, context) + } + + static func decode(from container: SingleValueDecodingContainer) throws -> Any { + if let value = try? container.decode(Bool.self) { + return value + } + if let value = try? container.decode(Int64.self) { + return value + } + if let value = try? container.decode(Double.self) { + return value + } + if let value = try? container.decode(String.self) { + return value + } + if container.decodeNil() { + return JSONNull() + } + throw decodingError(forCodingPath: container.codingPath) + } + + static func decode(from container: inout UnkeyedDecodingContainer) throws -> Any { + if let value = try? container.decode(Bool.self) { + return value + } + if let value = try? container.decode(Int64.self) { + return value + } + if let value = try? container.decode(Double.self) { + return value + } + if let value = try? container.decode(String.self) { + return value + } + if let value = try? container.decodeNil() { + if value { + return JSONNull() + } + } + if var container = try? container.nestedUnkeyedContainer() { + return try decodeArray(from: &container) + } + if var container = try? container.nestedContainer(keyedBy: JSONCodingKey.self) { + return try decodeDictionary(from: &container) + } + throw decodingError(forCodingPath: container.codingPath) + } + + static func decode(from container: inout KeyedDecodingContainer, forKey key: JSONCodingKey) throws -> Any { + if let value = try? container.decode(Bool.self, forKey: key) { + return value + } + if let value = try? container.decode(Int64.self, forKey: key) { + return value + } + if let value = try? container.decode(Double.self, forKey: key) { + return value + } + if let value = try? container.decode(String.self, forKey: key) { + return value + } + if let value = try? container.decodeNil(forKey: key) { + if value { + return JSONNull() + } + } + if var container = try? container.nestedUnkeyedContainer(forKey: key) { + return try decodeArray(from: &container) + } + if var container = try? container.nestedContainer(keyedBy: JSONCodingKey.self, forKey: key) { + return try decodeDictionary(from: &container) + } + throw decodingError(forCodingPath: container.codingPath) + } + + static func decodeArray(from container: inout UnkeyedDecodingContainer) throws -> [Any] { + var arr: [Any] = [] + while !container.isAtEnd { + let value = try decode(from: &container) + arr.append(value) + } + return arr + } + + static func decodeDictionary(from container: inout KeyedDecodingContainer) throws -> [String: Any] { + var dict = [String: Any]() + for key in container.allKeys { + let value = try decode(from: &container, forKey: key) + dict[key.stringValue] = value + } + return dict + } + + static func encode(to container: inout UnkeyedEncodingContainer, array: [Any]) throws { + for value in array { + if let value = value as? Bool { + try container.encode(value) + } else if let value = value as? Int64 { + try container.encode(value) + } else if let value = value as? Double { + try container.encode(value) + } else if let value = value as? String { + try container.encode(value) + } else if value is JSONNull { + try container.encodeNil() + } else if let value = value as? [Any] { + var container = container.nestedUnkeyedContainer() + try encode(to: &container, array: value) + } else if let value = value as? [String: Any] { + var container = container.nestedContainer(keyedBy: JSONCodingKey.self) + try encode(to: &container, dictionary: value) + } else { + throw encodingError(forValue: value, codingPath: container.codingPath) + } + } + } + + static func encode(to container: inout KeyedEncodingContainer, dictionary: [String: Any]) throws { + for (key, value) in dictionary { + let key = JSONCodingKey(stringValue: key)! + if let value = value as? Bool { + try container.encode(value, forKey: key) + } else if let value = value as? Int64 { + try container.encode(value, forKey: key) + } else if let value = value as? Double { + try container.encode(value, forKey: key) + } else if let value = value as? String { + try container.encode(value, forKey: key) + } else if value is JSONNull { + try container.encodeNil(forKey: key) + } else if let value = value as? [Any] { + var container = container.nestedUnkeyedContainer(forKey: key) + try encode(to: &container, array: value) + } else if let value = value as? [String: Any] { + var container = container.nestedContainer(keyedBy: JSONCodingKey.self, forKey: key) + try encode(to: &container, dictionary: value) + } else { + throw encodingError(forValue: value, codingPath: container.codingPath) + } + } + } + + static func encode(to container: inout SingleValueEncodingContainer, value: Any) throws { + if let value = value as? Bool { + try container.encode(value) + } else if let value = value as? Int64 { + try container.encode(value) + } else if let value = value as? Double { + try container.encode(value) + } else if let value = value as? String { + try container.encode(value) + } else if value is JSONNull { + try container.encodeNil() + } else { + throw encodingError(forValue: value, codingPath: container.codingPath) + } + } + + public required init(from decoder: Decoder) throws { + if var arrayContainer = try? decoder.unkeyedContainer() { + self.value = try JSONAny.decodeArray(from: &arrayContainer) + } else if var container = try? decoder.container(keyedBy: JSONCodingKey.self) { + self.value = try JSONAny.decodeDictionary(from: &container) + } else { + let container = try decoder.singleValueContainer() + self.value = try JSONAny.decode(from: container) + } + } + + public func encode(to encoder: Encoder) throws { + if let arr = self.value as? [Any] { + var container = encoder.unkeyedContainer() + try JSONAny.encode(to: &container, array: arr) + } else if let dict = self.value as? [String: Any] { + var container = encoder.container(keyedBy: JSONCodingKey.self) + try JSONAny.encode(to: &container, dictionary: dict) + } else { + var container = encoder.singleValueContainer() + try JSONAny.encode(to: &container, value: self.value) + } + } }`); } @@ -1472,20 +1165,20 @@ encoder.dateEncodingStrategy = .formatted(formatter)`); this.emitBlockWithAccess("extension DataRequest", () => { this .emitMultiline(`fileprivate func decodableResponseSerializer() -> DataResponseSerializer { - return DataResponseSerializer { _, response, data, error in - guard error == nil else { return .failure(error!) } - - guard let data = data else { - return .failure(AFError.responseSerializationFailed(reason: .inputDataNil)) - } - - return Result { try newJSONDecoder().decode(T.self, from: data) } - } + return DataResponseSerializer { _, response, data, error in + guard error == nil else { return .failure(error!) } + + guard let data = data else { + return .failure(AFError.responseSerializationFailed(reason: .inputDataNil)) + } + + return Result { try newJSONDecoder().decode(T.self, from: data) } + } } @discardableResult fileprivate func responseDecodable(queue: DispatchQueue? = nil, completionHandler: @escaping (DataResponse) -> Void) -> Self { - return response(queue: queue, responseSerializer: decodableResponseSerializer(), completionHandler: completionHandler) + return response(queue: queue, responseSerializer: decodableResponseSerializer(), completionHandler: completionHandler) }`); this.ensureBlankLine(); this.forEachTopLevel("leading-and-interposing", (_, name) => { diff --git a/packages/quicktype-core/src/language/Swift/constants.ts b/packages/quicktype-core/src/language/Swift/constants.ts new file mode 100644 index 000000000..4a8c4ee2f --- /dev/null +++ b/packages/quicktype-core/src/language/Swift/constants.ts @@ -0,0 +1,101 @@ +export const keywords = [ + "await", + "associatedtype", + "class", + "deinit", + "enum", + "extension", + "fileprivate", + "func", + "import", + "init", + "inout", + "internal", + "let", + "open", + "operator", + "private", + "protocol", + "public", + "static", + "struct", + "subscript", + "typealias", + "var", + "break", + "case", + "continue", + "default", + "defer", + "do", + "else", + "fallthrough", + "for", + "guard", + "if", + "in", + "repeat", + "return", + "switch", + "where", + "while", + "as", + "Any", + "catch", + "false", + "is", + "nil", + "rethrows", + "super", + "self", + "Self", + "throw", + "throws", + "true", + "try", + "_", + "associativity", + "convenience", + "dynamic", + "didSet", + "final", + "get", + "infix", + "indirect", + "lazy", + "left", + "mutating", + "nonmutating", + "optional", + "override", + "postfix", + "precedence", + "prefix", + "Protocol", + "required", + "right", + "set", + "Type", + "unowned", + "weak", + "willSet", + "String", + "Int", + "Double", + "Bool", + "Data", + "Date", + "URL", + "CommandLine", + "FileHandle", + "JSONSerialization", + "checkNull", + "removeNSNull", + "nilToNSNull", + "convertArray", + "convertOptional", + "convertDict", + "convertDouble", + "jsonString", + "jsonData" +] as const; diff --git a/packages/quicktype-core/src/language/Swift/index.ts b/packages/quicktype-core/src/language/Swift/index.ts new file mode 100644 index 000000000..902d4fff7 --- /dev/null +++ b/packages/quicktype-core/src/language/Swift/index.ts @@ -0,0 +1,2 @@ +export { SwiftTargetLanguage, swiftOptions } from "./language"; +export { SwiftRenderer } from "./SwiftRenderer"; diff --git a/packages/quicktype-core/src/language/Swift/language.ts b/packages/quicktype-core/src/language/Swift/language.ts new file mode 100644 index 000000000..152e932af --- /dev/null +++ b/packages/quicktype-core/src/language/Swift/language.ts @@ -0,0 +1,128 @@ +import { type DateTimeRecognizer } from "../../DateTime"; +import { type RenderContext } from "../../Renderer"; +import { BooleanOption, EnumOption, type Option, StringOption, getOptionValues } from "../../RendererOptions"; +import { AcronymStyleOptions, acronymOption } from "../../support/Acronyms"; +import { TargetLanguage } from "../../TargetLanguage"; +import { type PrimitiveStringTypeKind, type TransformedStringTypeKind } from "../../Type"; +import { type StringTypeMapping } from "../../TypeBuilder"; +import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../../types"; + +import { SwiftRenderer } from "./SwiftRenderer"; +import { SwiftDateTimeRecognizer } from "./utils"; + +export const swiftOptions = { + justTypes: new BooleanOption("just-types", "Plain types only", false), + convenienceInitializers: new BooleanOption("initializers", "Generate initializers and mutators", true), + explicitCodingKeys: new BooleanOption("coding-keys", "Explicit CodingKey values in Codable types", true), + codingKeysProtocol: new StringOption( + "coding-keys-protocol", + "CodingKeys implements protocols", + "protocol1, protocol2...", + "", + "secondary" + ), + alamofire: new BooleanOption("alamofire", "Alamofire extensions", false), + namedTypePrefix: new StringOption("type-prefix", "Prefix for type names", "PREFIX", "", "secondary"), + useClasses: new EnumOption("struct-or-class", "Structs or classes", [ + ["struct", false], + ["class", true] + ]), + mutableProperties: new BooleanOption("mutable-properties", "Use var instead of let for object properties", false), + acronymStyle: acronymOption(AcronymStyleOptions.Pascal), + dense: new EnumOption( + "density", + "Code density", + [ + ["dense", true], + ["normal", false] + ], + "dense", + "secondary" + ), + linux: new BooleanOption("support-linux", "Support Linux", false, "secondary"), + objcSupport: new BooleanOption( + "objective-c-support", + "Objects inherit from NSObject and @objcMembers is added to classes", + false + ), + optionalEnums: new BooleanOption("optional-enums", "If no matching case is found enum value is set to null", false), + swift5Support: new BooleanOption("swift-5-support", "Renders output in a Swift 5 compatible mode", false), + sendable: new BooleanOption("sendable", "Mark generated models as Sendable", false), + multiFileOutput: new BooleanOption( + "multi-file-output", + "Renders each top-level object in its own Swift file", + false + ), + accessLevel: new EnumOption( + "access-level", + "Access level", + [ + ["internal", "internal"], + ["public", "public"] + ], + "internal", + "secondary" + ), + protocol: new EnumOption( + "protocol", + "Make types implement protocol", + [ + ["none", { equatable: false, hashable: false }], + ["equatable", { equatable: true, hashable: false }], + ["hashable", { equatable: false, hashable: true }] + ], + "none", + "secondary" + ) +}; + +export class SwiftTargetLanguage extends TargetLanguage { + public constructor() { + super("Swift", ["swift", "swift4"], "swift"); + } + + protected getOptions(): Array> { + return [ + swiftOptions.justTypes, + swiftOptions.useClasses, + swiftOptions.dense, + swiftOptions.convenienceInitializers, + swiftOptions.explicitCodingKeys, + swiftOptions.codingKeysProtocol, + swiftOptions.accessLevel, + swiftOptions.alamofire, + swiftOptions.linux, + swiftOptions.namedTypePrefix, + swiftOptions.protocol, + swiftOptions.acronymStyle, + swiftOptions.objcSupport, + swiftOptions.optionalEnums, + swiftOptions.sendable, + swiftOptions.swift5Support, + swiftOptions.multiFileOutput, + swiftOptions.mutableProperties + ]; + } + + public get stringTypeMapping(): StringTypeMapping { + const mapping: Map = new Map(); + mapping.set("date-time", "date-time"); + return mapping; + } + + public get supportsOptionalClassProperties(): boolean { + return true; + } + + public get supportsUnionsWithBothNumberTypes(): boolean { + return true; + } + + protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): SwiftRenderer { + return new SwiftRenderer(this, renderContext, getOptionValues(swiftOptions, untypedOptionValues)); + } + + public get dateTimeRecognizer(): DateTimeRecognizer { + return new SwiftDateTimeRecognizer(); + } +} diff --git a/packages/quicktype-core/src/language/Swift/utils.ts b/packages/quicktype-core/src/language/Swift/utils.ts new file mode 100644 index 000000000..ce4e2fa8b --- /dev/null +++ b/packages/quicktype-core/src/language/Swift/utils.ts @@ -0,0 +1,84 @@ +import { DefaultDateTimeRecognizer } from "../../DateTime"; +import { type Name } from "../../Naming"; +import { type ForEachPosition } from "../../Renderer"; +import { + addPrefixIfNecessary, + allLowerWordStyle, + allUpperWordStyle, + combineWords, + escapeNonPrintableMapper, + firstUpperWordStyle, + intToHex, + isDigit, + isLetterOrUnderscore, + isNumeric, + isPrintable, + legalizeCharacters, + splitIntoWords, + utf32ConcatMap +} from "../../support/Strings"; +import { type ClassProperty } from "../../Type"; + +export const MAX_SAMELINE_PROPERTIES = 4; + +// These are all recognized by Swift as ISO8601 date-times: +// +// 2018-08-14T02:45:50+00:00 +// 2018-08-14T02:45:50+00 +// 2018-08-14T02:45:50+1 +// 2018-08-14T02:45:50+1111 +// 2018-08-14T02:45:50+1111:1:33 +// 2018-08-14T02:45:50-00 +// 2018-08-14T02:45:50z +// 2018-00008-1T002:45:3Z + +const swiftDateTimeRegex = /^\d+-\d+-\d+T\d+:\d+:\d+([zZ]|[+-]\d+(:\d+)?)$/; + +export class SwiftDateTimeRecognizer extends DefaultDateTimeRecognizer { + public isDateTime(str: string): boolean { + return swiftDateTimeRegex.exec(str) !== null; + } +} + +export interface SwiftProperty { + jsonName: string; + name: Name; + parameter: ClassProperty; + position: ForEachPosition; +} + +function isPartCharacter(codePoint: number): boolean { + return isLetterOrUnderscore(codePoint) || isNumeric(codePoint); +} + +function isStartCharacter(codePoint: number): boolean { + return isPartCharacter(codePoint) && !isDigit(codePoint); +} + +const legalizeName = legalizeCharacters(isPartCharacter); + +export function swiftNameStyle( + prefix: string, + isUpper: boolean, + original: string, + acronymsStyle: (s: string) => string = allUpperWordStyle +): string { + const words = splitIntoWords(original); + const combined = combineWords( + words, + legalizeName, + isUpper ? firstUpperWordStyle : allLowerWordStyle, + firstUpperWordStyle, + isUpper ? allUpperWordStyle : allLowerWordStyle, + acronymsStyle, + "", + isStartCharacter + ); + return addPrefixIfNecessary(prefix, combined); +} + +function unicodeEscape(codePoint: number): string { + return "\\u{" + intToHex(codePoint, 0) + "}"; +} + +export const stringEscape = utf32ConcatMap(escapeNonPrintableMapper(isPrintable, unicodeEscape)); diff --git a/packages/quicktype-core/src/language/TypeScriptEffectSchema.ts b/packages/quicktype-core/src/language/TypeScriptEffectSchema/TypeScriptEffectSchemaRenderer.ts similarity index 82% rename from packages/quicktype-core/src/language/TypeScriptEffectSchema.ts rename to packages/quicktype-core/src/language/TypeScriptEffectSchema/TypeScriptEffectSchemaRenderer.ts index c3844d847..f06807a39 100644 --- a/packages/quicktype-core/src/language/TypeScriptEffectSchema.ts +++ b/packages/quicktype-core/src/language/TypeScriptEffectSchema/TypeScriptEffectSchemaRenderer.ts @@ -1,11 +1,11 @@ import { arrayIntercalate } from "collection-utils"; -import { ConvenienceRenderer } from "../ConvenienceRenderer"; -import { type Name, type Namer, funPrefixNamer } from "../Naming"; -import { type RenderContext } from "../Renderer"; -import { BooleanOption, type Option, type OptionValues, getOptionValues } from "../RendererOptions"; -import { type Sourcelike } from "../Source"; -import { AcronymStyleOptions, acronymStyle } from "../support/Acronyms"; +import { ConvenienceRenderer } from "../../ConvenienceRenderer"; +import { type Name, type Namer, funPrefixNamer } from "../../Naming"; +import { type RenderContext } from "../../Renderer"; +import { type OptionValues } from "../../RendererOptions"; +import { type Sourcelike } from "../../Source"; +import { AcronymStyleOptions, acronymStyle } from "../../support/Acronyms"; import { allLowerWordStyle, capitalize, @@ -15,43 +15,14 @@ import { splitIntoWords, stringEscape, utf16StringEscape -} from "../support/Strings"; -import { panic } from "../support/Support"; -import { TargetLanguage } from "../TargetLanguage"; -import { ArrayType, type ClassProperty, EnumType, MapType, type ObjectType, type Type } from "../Type"; -import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../types"; -import { matchType } from "../TypeUtils"; - -import { legalizeName } from "./JavaScript"; - -export const typeScriptEffectSchemaOptions = { - justSchema: new BooleanOption("just-schema", "Schema only", false) -}; - -export class TypeScriptEffectSchemaTargetLanguage extends TargetLanguage { - protected getOptions(): Array> { - return []; - } - - public constructor( - displayName: string = "TypeScript Effect Schema", - names: string[] = ["typescript-effect-schema"], - extension: string = "ts" - ) { - super(displayName, names, extension); - } - - protected makeRenderer( - renderContext: RenderContext, - untypedOptionValues: FixMeOptionsType - ): TypeScriptEffectSchemaRenderer { - return new TypeScriptEffectSchemaRenderer( - this, - renderContext, - getOptionValues(typeScriptEffectSchemaOptions, untypedOptionValues) - ); - } -} +} from "../../support/Strings"; +import { panic } from "../../support/Support"; +import { type TargetLanguage } from "../../TargetLanguage"; +import { ArrayType, type ClassProperty, EnumType, MapType, type ObjectType, type Type } from "../../Type"; +import { matchType } from "../../TypeUtils"; +import { legalizeName } from "../JavaScript/utils"; + +import { type typeScriptEffectSchemaOptions } from "./language"; export class TypeScriptEffectSchemaRenderer extends ConvenienceRenderer { private emittedObjects = new Set(); diff --git a/packages/quicktype-core/src/language/TypeScriptEffectSchema/index.ts b/packages/quicktype-core/src/language/TypeScriptEffectSchema/index.ts new file mode 100644 index 000000000..ea8cf8149 --- /dev/null +++ b/packages/quicktype-core/src/language/TypeScriptEffectSchema/index.ts @@ -0,0 +1,2 @@ +export { TypeScriptEffectSchemaTargetLanguage, typeScriptEffectSchemaOptions } from "./language"; +export { TypeScriptEffectSchemaRenderer } from "./TypeScriptEffectSchemaRenderer"; diff --git a/packages/quicktype-core/src/language/TypeScriptEffectSchema/language.ts b/packages/quicktype-core/src/language/TypeScriptEffectSchema/language.ts new file mode 100644 index 000000000..5e311edb0 --- /dev/null +++ b/packages/quicktype-core/src/language/TypeScriptEffectSchema/language.ts @@ -0,0 +1,35 @@ +import { type RenderContext } from "../../Renderer"; +import { BooleanOption, type Option, getOptionValues } from "../../RendererOptions"; +import { TargetLanguage } from "../../TargetLanguage"; +import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../../types"; + +import { TypeScriptEffectSchemaRenderer } from "./TypeScriptEffectSchemaRenderer"; + +export const typeScriptEffectSchemaOptions = { + justSchema: new BooleanOption("just-schema", "Schema only", false) +}; + +export class TypeScriptEffectSchemaTargetLanguage extends TargetLanguage { + protected getOptions(): Array> { + return []; + } + + public constructor( + displayName: string = "TypeScript Effect Schema", + names: string[] = ["typescript-effect-schema"], + extension: string = "ts" + ) { + super(displayName, names, extension); + } + + protected makeRenderer( + renderContext: RenderContext, + untypedOptionValues: FixMeOptionsType + ): TypeScriptEffectSchemaRenderer { + return new TypeScriptEffectSchemaRenderer( + this, + renderContext, + getOptionValues(typeScriptEffectSchemaOptions, untypedOptionValues) + ); + } +} diff --git a/packages/quicktype-core/src/language/TypeScriptFlow.ts b/packages/quicktype-core/src/language/TypeScriptFlow.ts deleted file mode 100644 index 202b95b4e..000000000 --- a/packages/quicktype-core/src/language/TypeScriptFlow.ts +++ /dev/null @@ -1,406 +0,0 @@ -import { type Name, type Namer, funPrefixNamer } from "../Naming"; -import { type RenderContext } from "../Renderer"; -import { BooleanOption, type Option, type OptionValues, getOptionValues } from "../RendererOptions"; -import { type MultiWord, type Sourcelike, modifySource, multiWord, parenIfNeeded, singleWord } from "../Source"; -import { camelCase, utf16StringEscape } from "../support/Strings"; -import { defined, panic } from "../support/Support"; -import { type TargetLanguage } from "../TargetLanguage"; -import { ArrayType, type ClassType, EnumType, type Type, UnionType } from "../Type"; -import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../types"; -import { isNamedType, matchType, nullableFromUnion } from "../TypeUtils"; - -import { - JavaScriptRenderer, - JavaScriptTargetLanguage, - type JavaScriptTypeAnnotations, - javaScriptOptions, - legalizeName -} from "./JavaScript"; -import { isES3IdentifierStart } from "./JavaScriptUnicodeMaps"; - -export const tsFlowOptions = Object.assign({}, javaScriptOptions, { - justTypes: new BooleanOption("just-types", "Interfaces only", false), - nicePropertyNames: new BooleanOption("nice-property-names", "Transform property names to be JavaScripty", false), - declareUnions: new BooleanOption("explicit-unions", "Explicitly name unions", false), - preferUnions: new BooleanOption("prefer-unions", "Use union type instead of enum", false), - preferTypes: new BooleanOption("prefer-types", "Use types instead of interfaces", false), - preferConstValues: new BooleanOption( - "prefer-const-values", - "Use string instead of enum for string enums with single value", - false - ), - readonly: new BooleanOption("readonly", "Use readonly type members", false) -}); - -const tsFlowTypeAnnotations = { - any: ": any", - anyArray: ": any[]", - anyMap: ": { [k: string]: any }", - string: ": string", - stringArray: ": string[]", - boolean: ": boolean" -}; - -export abstract class TypeScriptFlowBaseTargetLanguage extends JavaScriptTargetLanguage { - protected getOptions(): Array> { - return [ - tsFlowOptions.justTypes, - tsFlowOptions.nicePropertyNames, - tsFlowOptions.declareUnions, - tsFlowOptions.runtimeTypecheck, - tsFlowOptions.runtimeTypecheckIgnoreUnknownProperties, - tsFlowOptions.acronymStyle, - tsFlowOptions.converters, - tsFlowOptions.rawType, - tsFlowOptions.preferUnions, - tsFlowOptions.preferTypes, - tsFlowOptions.preferConstValues, - tsFlowOptions.readonly - ]; - } - - public get supportsOptionalClassProperties(): boolean { - return true; - } - - protected abstract makeRenderer( - renderContext: RenderContext, - untypedOptionValues: FixMeOptionsType - ): JavaScriptRenderer; -} - -export class TypeScriptTargetLanguage extends TypeScriptFlowBaseTargetLanguage { - public constructor() { - super("TypeScript", ["typescript", "ts", "tsx"], "ts"); - } - - protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): TypeScriptRenderer { - return new TypeScriptRenderer(this, renderContext, getOptionValues(tsFlowOptions, untypedOptionValues)); - } -} - -function quotePropertyName(original: string): string { - const escaped = utf16StringEscape(original); - const quoted = `"${escaped}"`; - - if (original.length === 0) { - return quoted; - } else if (!isES3IdentifierStart(original.codePointAt(0) as number)) { - return quoted; - } else if (escaped !== original) { - return quoted; - } else if (legalizeName(original) !== original) { - return quoted; - } else { - return original; - } -} - -export abstract class TypeScriptFlowBaseRenderer extends JavaScriptRenderer { - public constructor( - targetLanguage: TargetLanguage, - renderContext: RenderContext, - protected readonly _tsFlowOptions: OptionValues - ) { - super(targetLanguage, renderContext, _tsFlowOptions); - } - - protected namerForObjectProperty(): Namer { - if (this._tsFlowOptions.nicePropertyNames) { - return funPrefixNamer("properties", s => this.nameStyle(s, false)); - } else { - return super.namerForObjectProperty(); - } - } - - protected sourceFor(t: Type): MultiWord { - if (this._tsFlowOptions.preferConstValues && t.kind === "enum" && t instanceof EnumType && t.cases.size === 1) { - const item = t.cases.values().next().value; - return singleWord(`"${utf16StringEscape(item)}"`); - } - - if (["class", "object", "enum"].includes(t.kind)) { - return singleWord(this.nameForNamedType(t)); - } - - return matchType( - t, - _anyType => singleWord("any"), - _nullType => singleWord("null"), - _boolType => singleWord("boolean"), - _integerType => singleWord("number"), - _doubleType => singleWord("number"), - _stringType => singleWord("string"), - arrayType => { - const itemType = this.sourceFor(arrayType.items); - if ( - (arrayType.items instanceof UnionType && !this._tsFlowOptions.declareUnions) || - arrayType.items instanceof ArrayType - ) { - return singleWord(["Array<", itemType.source, ">"]); - } else { - return singleWord([parenIfNeeded(itemType), "[]"]); - } - }, - _classType => panic("We handled this above"), - mapType => singleWord(["{ [key: string]: ", this.sourceFor(mapType.values).source, " }"]), - _enumType => panic("We handled this above"), - unionType => { - if (!this._tsFlowOptions.declareUnions || nullableFromUnion(unionType) !== null) { - const children = Array.from(unionType.getChildren()).map(c => parenIfNeeded(this.sourceFor(c))); - return multiWord(" | ", ...children); - } else { - return singleWord(this.nameForNamedType(unionType)); - } - }, - transformedStringType => { - if (transformedStringType.kind === "date-time") { - return singleWord("Date"); - } - - return singleWord("string"); - } - ); - } - - protected abstract emitEnum(e: EnumType, enumName: Name): void; - - protected abstract emitClassBlock(c: ClassType, className: Name): void; - - protected emitClassBlockBody(c: ClassType): void { - this.emitPropertyTable(c, (name, _jsonName, p) => { - const t = p.type; - - let propertyName: Sourcelike = name; - propertyName = modifySource(quotePropertyName, name); - - if (this._tsFlowOptions.readonly) { - propertyName = modifySource(_propertyName => "readonly " + _propertyName, propertyName); - } - - return [ - [propertyName, p.isOptional ? "?" : "", ": "], - [this.sourceFor(t).source, ";"] - ]; - }); - - const additionalProperties = c.getAdditionalProperties(); - if (additionalProperties) { - this.emitTable([["[property: string]", ": ", this.sourceFor(additionalProperties).source, ";"]]); - } - } - - private emitClass(c: ClassType, className: Name): void { - this.emitDescription(this.descriptionForType(c)); - this.emitClassBlock(c, className); - } - - protected emitUnion(u: UnionType, unionName: Name): void { - if (!this._tsFlowOptions.declareUnions) { - return; - } - - this.emitDescription(this.descriptionForType(u)); - - const children = multiWord(" | ", ...Array.from(u.getChildren()).map(c => parenIfNeeded(this.sourceFor(c)))); - this.emitLine("export type ", unionName, " = ", children.source, ";"); - } - - protected emitTypes(): void { - // emit primitive top levels - this.forEachTopLevel("none", (t, name) => { - if (!t.isPrimitive()) { - return; - } - - this.ensureBlankLine(); - this.emitDescription(this.descriptionForType(t)); - this.emitLine("type ", name, " = ", this.sourceFor(t).source, ";"); - }); - - this.forEachNamedType( - "leading-and-interposing", - (c: ClassType, n: Name) => this.emitClass(c, n), - (e, n) => this.emitEnum(e, n), - (u, n) => this.emitUnion(u, n) - ); - } - - protected emitUsageComments(): void { - if (this._tsFlowOptions.justTypes) return; - super.emitUsageComments(); - } - - protected deserializerFunctionLine(t: Type, name: Name): Sourcelike { - const jsonType = this._tsFlowOptions.rawType === "json" ? "string" : "any"; - return ["function to", name, "(json: ", jsonType, "): ", this.sourceFor(t).source]; - } - - protected serializerFunctionLine(t: Type, name: Name): Sourcelike { - const camelCaseName = modifySource(camelCase, name); - const returnType = this._tsFlowOptions.rawType === "json" ? "string" : "any"; - return ["function ", camelCaseName, "ToJson(value: ", this.sourceFor(t).source, "): ", returnType]; - } - - protected get moduleLine(): string | undefined { - return undefined; - } - - protected get castFunctionLines(): [string, string] { - return ["function cast(val: any, typ: any): T", "function uncast(val: T, typ: any): any"]; - } - - protected get typeAnnotations(): JavaScriptTypeAnnotations { - throw new Error("not implemented"); - } - - protected emitConvertModule(): void { - if (this._tsFlowOptions.justTypes) return; - super.emitConvertModule(); - } - - protected emitConvertModuleHelpers(): void { - if (this._tsFlowOptions.justTypes) return; - super.emitConvertModuleHelpers(); - } - - protected emitModuleExports(): void { - if (this._tsFlowOptions.justTypes) { - return; - } else { - super.emitModuleExports(); - } - } -} - -export class TypeScriptRenderer extends TypeScriptFlowBaseRenderer { - protected forbiddenNamesForGlobalNamespace(): string[] { - return ["Array", "Date"]; - } - - protected deserializerFunctionLine(t: Type, name: Name): Sourcelike { - const jsonType = this._tsFlowOptions.rawType === "json" ? "string" : "any"; - return ["public static to", name, "(json: ", jsonType, "): ", this.sourceFor(t).source]; - } - - protected serializerFunctionLine(t: Type, name: Name): Sourcelike { - const camelCaseName = modifySource(camelCase, name); - const returnType = this._tsFlowOptions.rawType === "json" ? "string" : "any"; - return ["public static ", camelCaseName, "ToJson(value: ", this.sourceFor(t).source, "): ", returnType]; - } - - protected get moduleLine(): string | undefined { - return "export class Convert"; - } - - protected get typeAnnotations(): JavaScriptTypeAnnotations { - return Object.assign({ never: ": never" }, tsFlowTypeAnnotations); - } - - protected emitModuleExports(): void { - return; - } - - protected emitUsageImportComment(): void { - const topLevelNames: Sourcelike[] = []; - this.forEachTopLevel( - "none", - (_t, name) => { - topLevelNames.push(", ", name); - }, - isNamedType - ); - this.emitLine("// import { Convert", topLevelNames, ' } from "./file";'); - } - - protected emitEnum(e: EnumType, enumName: Name): void { - this.emitDescription(this.descriptionForType(e)); - - // enums with only one value are emitted as constants - if (this._tsFlowOptions.preferConstValues && e.cases.size === 1) return; - - if (this._tsFlowOptions.preferUnions) { - let items = ""; - e.cases.forEach(item => { - if (items === "") { - items += `"${utf16StringEscape(item)}"`; - return; - } - - items += ` | "${utf16StringEscape(item)}"`; - }); - this.emitLine("export type ", enumName, " = ", items, ";"); - } else { - this.emitBlock(["export enum ", enumName, " "], "", () => { - this.forEachEnumCase(e, "none", (name, jsonName) => { - this.emitLine(name, ` = "${utf16StringEscape(jsonName)}",`); - }); - }); - } - } - - protected emitClassBlock(c: ClassType, className: Name): void { - this.emitBlock( - this._tsFlowOptions.preferTypes - ? ["export type ", className, " = "] - : ["export interface ", className, " "], - "", - () => { - this.emitClassBlockBody(c); - } - ); - } - - protected emitSourceStructure() { - super.emitSourceStructure(); - } -} - -export class FlowTargetLanguage extends TypeScriptFlowBaseTargetLanguage { - public constructor() { - super("Flow", ["flow"], "js"); - } - - protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): FlowRenderer { - return new FlowRenderer(this, renderContext, getOptionValues(tsFlowOptions, untypedOptionValues)); - } -} - -export class FlowRenderer extends TypeScriptFlowBaseRenderer { - protected forbiddenNamesForGlobalNamespace(): string[] { - return ["Class", "Date", "Object", "String", "Array", "JSON", "Error"]; - } - - protected get typeAnnotations(): JavaScriptTypeAnnotations { - return Object.assign({ never: "" }, tsFlowTypeAnnotations); - } - - protected emitEnum(e: EnumType, enumName: Name): void { - this.emitDescription(this.descriptionForType(e)); - const lines: string[][] = []; - this.forEachEnumCase(e, "none", (_, jsonName) => { - const maybeOr = lines.length === 0 ? " " : "| "; - lines.push([maybeOr, '"', utf16StringEscape(jsonName), '"']); - }); - defined(lines[lines.length - 1]).push(";"); - - this.emitLine("export type ", enumName, " ="); - this.indent(() => { - for (const line of lines) { - this.emitLine(line); - } - }); - } - - protected emitClassBlock(c: ClassType, className: Name): void { - this.emitBlock(["export type ", className, " = "], ";", () => { - this.emitClassBlockBody(c); - }); - } - - protected emitSourceStructure(): void { - this.emitLine("// @flow"); - this.ensureBlankLine(); - super.emitSourceStructure(); - } -} diff --git a/packages/quicktype-core/src/language/TypeScriptFlow/FlowRenderer.ts b/packages/quicktype-core/src/language/TypeScriptFlow/FlowRenderer.ts new file mode 100644 index 000000000..eb4695583 --- /dev/null +++ b/packages/quicktype-core/src/language/TypeScriptFlow/FlowRenderer.ts @@ -0,0 +1,47 @@ +import { type Name } from "../../Naming"; +import { utf16StringEscape } from "../../support/Strings"; +import { defined } from "../../support/Support"; +import { type ClassType, type EnumType } from "../../Type"; +import { type JavaScriptTypeAnnotations } from "../JavaScript"; + +import { TypeScriptFlowBaseRenderer } from "./TypeScriptFlowBaseRenderer"; +import { tsFlowTypeAnnotations } from "./utils"; + +export class FlowRenderer extends TypeScriptFlowBaseRenderer { + protected forbiddenNamesForGlobalNamespace(): string[] { + return ["Class", "Date", "Object", "String", "Array", "JSON", "Error"]; + } + + protected get typeAnnotations(): JavaScriptTypeAnnotations { + return Object.assign({ never: "" }, tsFlowTypeAnnotations); + } + + protected emitEnum(e: EnumType, enumName: Name): void { + this.emitDescription(this.descriptionForType(e)); + const lines: string[][] = []; + this.forEachEnumCase(e, "none", (_, jsonName) => { + const maybeOr = lines.length === 0 ? " " : "| "; + lines.push([maybeOr, '"', utf16StringEscape(jsonName), '"']); + }); + defined(lines[lines.length - 1]).push(";"); + + this.emitLine("export type ", enumName, " ="); + this.indent(() => { + for (const line of lines) { + this.emitLine(line); + } + }); + } + + protected emitClassBlock(c: ClassType, className: Name): void { + this.emitBlock(["export type ", className, " = "], ";", () => { + this.emitClassBlockBody(c); + }); + } + + protected emitSourceStructure(): void { + this.emitLine("// @flow"); + this.ensureBlankLine(); + super.emitSourceStructure(); + } +} diff --git a/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptFlowBaseRenderer.ts b/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptFlowBaseRenderer.ts new file mode 100644 index 000000000..3cc9b09bf --- /dev/null +++ b/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptFlowBaseRenderer.ts @@ -0,0 +1,190 @@ +import { type Name, type Namer, funPrefixNamer } from "../../Naming"; +import { type RenderContext } from "../../Renderer"; +import { type OptionValues } from "../../RendererOptions"; +import { type MultiWord, type Sourcelike, modifySource, multiWord, parenIfNeeded, singleWord } from "../../Source"; +import { camelCase, utf16StringEscape } from "../../support/Strings"; +import { panic } from "../../support/Support"; +import { type TargetLanguage } from "../../TargetLanguage"; +import { ArrayType, type ClassType, EnumType, type Type, UnionType } from "../../Type"; +import { matchType, nullableFromUnion } from "../../TypeUtils"; +import { JavaScriptRenderer, type JavaScriptTypeAnnotations } from "../JavaScript"; + +import { type tsFlowOptions } from "./language"; +import { quotePropertyName } from "./utils"; + +export abstract class TypeScriptFlowBaseRenderer extends JavaScriptRenderer { + public constructor( + targetLanguage: TargetLanguage, + renderContext: RenderContext, + protected readonly _tsFlowOptions: OptionValues + ) { + super(targetLanguage, renderContext, _tsFlowOptions); + } + + protected namerForObjectProperty(): Namer { + if (this._tsFlowOptions.nicePropertyNames) { + return funPrefixNamer("properties", s => this.nameStyle(s, false)); + } else { + return super.namerForObjectProperty(); + } + } + + protected sourceFor(t: Type): MultiWord { + if (this._tsFlowOptions.preferConstValues && t.kind === "enum" && t instanceof EnumType && t.cases.size === 1) { + const item = t.cases.values().next().value; + return singleWord(`"${utf16StringEscape(item)}"`); + } + + if (["class", "object", "enum"].includes(t.kind)) { + return singleWord(this.nameForNamedType(t)); + } + + return matchType( + t, + _anyType => singleWord("any"), + _nullType => singleWord("null"), + _boolType => singleWord("boolean"), + _integerType => singleWord("number"), + _doubleType => singleWord("number"), + _stringType => singleWord("string"), + arrayType => { + const itemType = this.sourceFor(arrayType.items); + if ( + (arrayType.items instanceof UnionType && !this._tsFlowOptions.declareUnions) || + arrayType.items instanceof ArrayType + ) { + return singleWord(["Array<", itemType.source, ">"]); + } else { + return singleWord([parenIfNeeded(itemType), "[]"]); + } + }, + _classType => panic("We handled this above"), + mapType => singleWord(["{ [key: string]: ", this.sourceFor(mapType.values).source, " }"]), + _enumType => panic("We handled this above"), + unionType => { + if (!this._tsFlowOptions.declareUnions || nullableFromUnion(unionType) !== null) { + const children = Array.from(unionType.getChildren()).map(c => parenIfNeeded(this.sourceFor(c))); + return multiWord(" | ", ...children); + } else { + return singleWord(this.nameForNamedType(unionType)); + } + }, + transformedStringType => { + if (transformedStringType.kind === "date-time") { + return singleWord("Date"); + } + + return singleWord("string"); + } + ); + } + + protected abstract emitEnum(e: EnumType, enumName: Name): void; + + protected abstract emitClassBlock(c: ClassType, className: Name): void; + + protected emitClassBlockBody(c: ClassType): void { + this.emitPropertyTable(c, (name, _jsonName, p) => { + const t = p.type; + + let propertyName: Sourcelike = name; + propertyName = modifySource(quotePropertyName, name); + + if (this._tsFlowOptions.readonly) { + propertyName = modifySource(_propertyName => "readonly " + _propertyName, propertyName); + } + + return [ + [propertyName, p.isOptional ? "?" : "", ": "], + [this.sourceFor(t).source, ";"] + ]; + }); + + const additionalProperties = c.getAdditionalProperties(); + if (additionalProperties) { + this.emitTable([["[property: string]", ": ", this.sourceFor(additionalProperties).source, ";"]]); + } + } + + private emitClass(c: ClassType, className: Name): void { + this.emitDescription(this.descriptionForType(c)); + this.emitClassBlock(c, className); + } + + protected emitUnion(u: UnionType, unionName: Name): void { + if (!this._tsFlowOptions.declareUnions) { + return; + } + + this.emitDescription(this.descriptionForType(u)); + + const children = multiWord(" | ", ...Array.from(u.getChildren()).map(c => parenIfNeeded(this.sourceFor(c)))); + this.emitLine("export type ", unionName, " = ", children.source, ";"); + } + + protected emitTypes(): void { + // emit primitive top levels + this.forEachTopLevel("none", (t, name) => { + if (!t.isPrimitive()) { + return; + } + + this.ensureBlankLine(); + this.emitDescription(this.descriptionForType(t)); + this.emitLine("type ", name, " = ", this.sourceFor(t).source, ";"); + }); + + this.forEachNamedType( + "leading-and-interposing", + (c: ClassType, n: Name) => this.emitClass(c, n), + (e, n) => this.emitEnum(e, n), + (u, n) => this.emitUnion(u, n) + ); + } + + protected emitUsageComments(): void { + if (this._tsFlowOptions.justTypes) return; + super.emitUsageComments(); + } + + protected deserializerFunctionLine(t: Type, name: Name): Sourcelike { + const jsonType = this._tsFlowOptions.rawType === "json" ? "string" : "any"; + return ["function to", name, "(json: ", jsonType, "): ", this.sourceFor(t).source]; + } + + protected serializerFunctionLine(t: Type, name: Name): Sourcelike { + const camelCaseName = modifySource(camelCase, name); + const returnType = this._tsFlowOptions.rawType === "json" ? "string" : "any"; + return ["function ", camelCaseName, "ToJson(value: ", this.sourceFor(t).source, "): ", returnType]; + } + + protected get moduleLine(): string | undefined { + return undefined; + } + + protected get castFunctionLines(): [string, string] { + return ["function cast(val: any, typ: any): T", "function uncast(val: T, typ: any): any"]; + } + + protected get typeAnnotations(): JavaScriptTypeAnnotations { + throw new Error("not implemented"); + } + + protected emitConvertModule(): void { + if (this._tsFlowOptions.justTypes) return; + super.emitConvertModule(); + } + + protected emitConvertModuleHelpers(): void { + if (this._tsFlowOptions.justTypes) return; + super.emitConvertModuleHelpers(); + } + + protected emitModuleExports(): void { + if (this._tsFlowOptions.justTypes) { + return; + } else { + super.emitModuleExports(); + } + } +} diff --git a/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptRenderer.ts b/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptRenderer.ts new file mode 100644 index 000000000..74edd55ed --- /dev/null +++ b/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptRenderer.ts @@ -0,0 +1,88 @@ +import { type Name } from "../../Naming"; +import { type Sourcelike, modifySource } from "../../Source"; +import { camelCase, utf16StringEscape } from "../../support/Strings"; +import { type ClassType, type EnumType, type Type } from "../../Type"; +import { isNamedType } from "../../TypeUtils"; +import { type JavaScriptTypeAnnotations } from "../JavaScript"; + +import { TypeScriptFlowBaseRenderer } from "./TypeScriptFlowBaseRenderer"; +import { tsFlowTypeAnnotations } from "./utils"; + +export class TypeScriptRenderer extends TypeScriptFlowBaseRenderer { + protected forbiddenNamesForGlobalNamespace(): string[] { + return ["Array", "Date"]; + } + + protected deserializerFunctionLine(t: Type, name: Name): Sourcelike { + const jsonType = this._tsFlowOptions.rawType === "json" ? "string" : "any"; + return ["public static to", name, "(json: ", jsonType, "): ", this.sourceFor(t).source]; + } + + protected serializerFunctionLine(t: Type, name: Name): Sourcelike { + const camelCaseName = modifySource(camelCase, name); + const returnType = this._tsFlowOptions.rawType === "json" ? "string" : "any"; + return ["public static ", camelCaseName, "ToJson(value: ", this.sourceFor(t).source, "): ", returnType]; + } + + protected get moduleLine(): string | undefined { + return "export class Convert"; + } + + protected get typeAnnotations(): JavaScriptTypeAnnotations { + return Object.assign({ never: ": never" }, tsFlowTypeAnnotations); + } + + protected emitModuleExports(): void { + return; + } + + protected emitUsageImportComment(): void { + const topLevelNames: Sourcelike[] = []; + this.forEachTopLevel( + "none", + (_t, name) => { + topLevelNames.push(", ", name); + }, + isNamedType + ); + this.emitLine("// import { Convert", topLevelNames, ' } from "./file";'); + } + + protected emitEnum(e: EnumType, enumName: Name): void { + this.emitDescription(this.descriptionForType(e)); + + // enums with only one value are emitted as constants + if (this._tsFlowOptions.preferConstValues && e.cases.size === 1) return; + + if (this._tsFlowOptions.preferUnions) { + let items = ""; + e.cases.forEach(item => { + if (items === "") { + items += `"${utf16StringEscape(item)}"`; + return; + } + + items += ` | "${utf16StringEscape(item)}"`; + }); + this.emitLine("export type ", enumName, " = ", items, ";"); + } else { + this.emitBlock(["export enum ", enumName, " "], "", () => { + this.forEachEnumCase(e, "none", (name, jsonName) => { + this.emitLine(name, ` = "${utf16StringEscape(jsonName)}",`); + }); + }); + } + } + + protected emitClassBlock(c: ClassType, className: Name): void { + this.emitBlock( + this._tsFlowOptions.preferTypes + ? ["export type ", className, " = "] + : ["export interface ", className, " "], + "", + () => { + this.emitClassBlockBody(c); + } + ); + } +} diff --git a/packages/quicktype-core/src/language/TypeScriptFlow/index.ts b/packages/quicktype-core/src/language/TypeScriptFlow/index.ts new file mode 100644 index 000000000..681acaa34 --- /dev/null +++ b/packages/quicktype-core/src/language/TypeScriptFlow/index.ts @@ -0,0 +1,3 @@ +export { FlowRenderer } from "./FlowRenderer"; +export { TypeScriptRenderer } from "./TypeScriptRenderer"; +export { FlowTargetLanguage, TypeScriptTargetLanguage, tsFlowOptions } from "./language"; diff --git a/packages/quicktype-core/src/language/TypeScriptFlow/language.ts b/packages/quicktype-core/src/language/TypeScriptFlow/language.ts new file mode 100644 index 000000000..8f8c37af5 --- /dev/null +++ b/packages/quicktype-core/src/language/TypeScriptFlow/language.ts @@ -0,0 +1,68 @@ +import { type RenderContext } from "../../Renderer"; +import { BooleanOption, type Option, getOptionValues } from "../../RendererOptions"; +import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../../types"; +import { type JavaScriptRenderer, JavaScriptTargetLanguage, javaScriptOptions } from "../JavaScript"; + +import { FlowRenderer } from "./FlowRenderer"; +import { TypeScriptRenderer } from "./TypeScriptRenderer"; + +export const tsFlowOptions = Object.assign({}, javaScriptOptions, { + justTypes: new BooleanOption("just-types", "Interfaces only", false), + nicePropertyNames: new BooleanOption("nice-property-names", "Transform property names to be JavaScripty", false), + declareUnions: new BooleanOption("explicit-unions", "Explicitly name unions", false), + preferUnions: new BooleanOption("prefer-unions", "Use union type instead of enum", false), + preferTypes: new BooleanOption("prefer-types", "Use types instead of interfaces", false), + preferConstValues: new BooleanOption( + "prefer-const-values", + "Use string instead of enum for string enums with single value", + false + ), + readonly: new BooleanOption("readonly", "Use readonly type members", false) +}); + +export abstract class TypeScriptFlowBaseTargetLanguage extends JavaScriptTargetLanguage { + protected getOptions(): Array> { + return [ + tsFlowOptions.justTypes, + tsFlowOptions.nicePropertyNames, + tsFlowOptions.declareUnions, + tsFlowOptions.runtimeTypecheck, + tsFlowOptions.runtimeTypecheckIgnoreUnknownProperties, + tsFlowOptions.acronymStyle, + tsFlowOptions.converters, + tsFlowOptions.rawType, + tsFlowOptions.preferUnions, + tsFlowOptions.preferTypes, + tsFlowOptions.preferConstValues, + tsFlowOptions.readonly + ]; + } + + public get supportsOptionalClassProperties(): boolean { + return true; + } + + protected abstract makeRenderer( + renderContext: RenderContext, + untypedOptionValues: FixMeOptionsType + ): JavaScriptRenderer; +} + +export class TypeScriptTargetLanguage extends TypeScriptFlowBaseTargetLanguage { + public constructor() { + super("TypeScript", ["typescript", "ts", "tsx"], "ts"); + } + + protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): TypeScriptRenderer { + return new TypeScriptRenderer(this, renderContext, getOptionValues(tsFlowOptions, untypedOptionValues)); + } +} +export class FlowTargetLanguage extends TypeScriptFlowBaseTargetLanguage { + public constructor() { + super("Flow", ["flow"], "js"); + } + + protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): FlowRenderer { + return new FlowRenderer(this, renderContext, getOptionValues(tsFlowOptions, untypedOptionValues)); + } +} diff --git a/packages/quicktype-core/src/language/TypeScriptFlow/utils.ts b/packages/quicktype-core/src/language/TypeScriptFlow/utils.ts new file mode 100644 index 000000000..b15b7eb1d --- /dev/null +++ b/packages/quicktype-core/src/language/TypeScriptFlow/utils.ts @@ -0,0 +1,29 @@ +import { utf16StringEscape } from "../../support/Strings"; +import { isES3IdentifierStart } from "../JavaScript/unicodeMaps"; +import { legalizeName } from "../JavaScript/utils"; + +export const tsFlowTypeAnnotations = { + any: ": any", + anyArray: ": any[]", + anyMap: ": { [k: string]: any }", + string: ": string", + stringArray: ": string[]", + boolean: ": boolean" +}; + +export function quotePropertyName(original: string): string { + const escaped = utf16StringEscape(original); + const quoted = `"${escaped}"`; + + if (original.length === 0) { + return quoted; + } else if (!isES3IdentifierStart(original.codePointAt(0) as number)) { + return quoted; + } else if (escaped !== original) { + return quoted; + } else if (legalizeName(original) !== original) { + return quoted; + } else { + return original; + } +} diff --git a/packages/quicktype-core/src/language/TypeScriptZod.ts b/packages/quicktype-core/src/language/TypeScriptZod/TypeScriptZodRenderer.ts similarity index 85% rename from packages/quicktype-core/src/language/TypeScriptZod.ts rename to packages/quicktype-core/src/language/TypeScriptZod/TypeScriptZodRenderer.ts index ada7494a9..a886288aa 100644 --- a/packages/quicktype-core/src/language/TypeScriptZod.ts +++ b/packages/quicktype-core/src/language/TypeScriptZod/TypeScriptZodRenderer.ts @@ -1,11 +1,11 @@ import { arrayIntercalate } from "collection-utils"; -import { ConvenienceRenderer } from "../ConvenienceRenderer"; -import { type Name, type Namer, funPrefixNamer } from "../Naming"; -import { type RenderContext } from "../Renderer"; -import { BooleanOption, type Option, type OptionValues, getOptionValues } from "../RendererOptions"; -import { type Sourcelike } from "../Source"; -import { AcronymStyleOptions, acronymStyle } from "../support/Acronyms"; +import { ConvenienceRenderer } from "../../ConvenienceRenderer"; +import { type Name, type Namer, funPrefixNamer } from "../../Naming"; +import { type RenderContext } from "../../Renderer"; +import { type OptionValues } from "../../RendererOptions"; +import { type Sourcelike } from "../../Source"; +import { AcronymStyleOptions, acronymStyle } from "../../support/Acronyms"; import { allLowerWordStyle, capitalize, @@ -15,62 +15,22 @@ import { splitIntoWords, stringEscape, utf16StringEscape -} from "../support/Strings"; -import { panic } from "../support/Support"; -import { TargetLanguage } from "../TargetLanguage"; +} from "../../support/Strings"; +import { panic } from "../../support/Support"; +import { type TargetLanguage } from "../../TargetLanguage"; import { ArrayType, type ClassProperty, ClassType, type EnumType, ObjectType, - type PrimitiveStringTypeKind, SetOperationType, - type TransformedStringTypeKind, type Type -} from "../Type"; -import { type StringTypeMapping } from "../TypeBuilder"; -import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../types"; -import { matchType } from "../TypeUtils"; +} from "../../Type"; +import { matchType } from "../../TypeUtils"; +import { legalizeName } from "../JavaScript/utils"; -import { legalizeName } from "./JavaScript"; - -export const typeScriptZodOptions = { - justSchema: new BooleanOption("just-schema", "Schema only", false) -}; - -export class TypeScriptZodTargetLanguage extends TargetLanguage { - protected getOptions(): Array> { - return []; - } - - public constructor( - displayName: string = "TypeScript Zod", - names: string[] = ["typescript-zod"], - extension: string = "ts" - ) { - super(displayName, names, extension); - } - - public get stringTypeMapping(): StringTypeMapping { - const mapping: Map = new Map(); - const dateTimeType = "date-time"; - mapping.set("date-time", dateTimeType); - return mapping; - } - - public get supportsOptionalClassProperties(): boolean { - return true; - } - - protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): TypeScriptZodRenderer { - return new TypeScriptZodRenderer( - this, - renderContext, - getOptionValues(typeScriptZodOptions, untypedOptionValues) - ); - } -} +import { type typeScriptZodOptions } from "./language"; export class TypeScriptZodRenderer extends ConvenienceRenderer { public constructor( diff --git a/packages/quicktype-core/src/language/TypeScriptZod/index.ts b/packages/quicktype-core/src/language/TypeScriptZod/index.ts new file mode 100644 index 000000000..657c2f91c --- /dev/null +++ b/packages/quicktype-core/src/language/TypeScriptZod/index.ts @@ -0,0 +1,2 @@ +export { TypeScriptZodTargetLanguage, typeScriptZodOptions } from "./language"; +export { TypeScriptZodRenderer } from "./TypeScriptZodRenderer"; diff --git a/packages/quicktype-core/src/language/TypeScriptZod/language.ts b/packages/quicktype-core/src/language/TypeScriptZod/language.ts new file mode 100644 index 000000000..a22f1298a --- /dev/null +++ b/packages/quicktype-core/src/language/TypeScriptZod/language.ts @@ -0,0 +1,45 @@ +import { type RenderContext } from "../../Renderer"; +import { BooleanOption, type Option, getOptionValues } from "../../RendererOptions"; +import { TargetLanguage } from "../../TargetLanguage"; +import { type PrimitiveStringTypeKind, type TransformedStringTypeKind } from "../../Type"; +import { type StringTypeMapping } from "../../TypeBuilder"; +import { type FixMeOptionsAnyType, type FixMeOptionsType } from "../../types"; + +import { TypeScriptZodRenderer } from "./TypeScriptZodRenderer"; + +export const typeScriptZodOptions = { + justSchema: new BooleanOption("just-schema", "Schema only", false) +}; + +export class TypeScriptZodTargetLanguage extends TargetLanguage { + protected getOptions(): Array> { + return []; + } + + public constructor( + displayName: string = "TypeScript Zod", + names: string[] = ["typescript-zod"], + extension: string = "ts" + ) { + super(displayName, names, extension); + } + + public get stringTypeMapping(): StringTypeMapping { + const mapping: Map = new Map(); + const dateTimeType = "date-time"; + mapping.set("date-time", dateTimeType); + return mapping; + } + + public get supportsOptionalClassProperties(): boolean { + return true; + } + + protected makeRenderer(renderContext: RenderContext, untypedOptionValues: FixMeOptionsType): TypeScriptZodRenderer { + return new TypeScriptZodRenderer( + this, + renderContext, + getOptionValues(typeScriptZodOptions, untypedOptionValues) + ); + } +} diff --git a/packages/quicktype-core/src/language/index.ts b/packages/quicktype-core/src/language/index.ts new file mode 100644 index 000000000..024a52494 --- /dev/null +++ b/packages/quicktype-core/src/language/index.ts @@ -0,0 +1,26 @@ +export * from "./CJSON"; +export * from "./CPlusPlus"; +export * from "./Crystal"; +export * from "./CSharp"; +export * from "./Dart"; +export * from "./Elixir"; +export * from "./Elm"; +export * from "./Golang"; +export * from "./Haskell"; +export * from "./Java"; +export * from "./JavaScript"; +export * from "./JavaScriptPropTypes"; +export * from "./JSONSchema"; +export * from "./Kotlin"; +export * from "./Objective-C"; +export * from "./Php"; +export * from "./Pike"; +export * from "./Python"; +export * from "./Ruby"; +export * from "./Rust"; +export * from "./Scala3"; +export * from "./Smithy4s"; +export * from "./Swift"; +export * from "./TypeScriptFlow"; +export * from "./TypeScriptEffectSchema"; +export * from "./TypeScriptZod"; diff --git a/test/languages.ts b/test/languages.ts index 51f76bb4a..f1890a7d5 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -70,7 +70,7 @@ export const CSharpLanguage: Language = { { "number-type": "decimal" }, { "any-type": "dynamic" } ], - sourceFiles: ["src/language/CSharp.ts"] + sourceFiles: ["src/language/CSharp/index.ts"] }; export const CSharpLanguageSystemTextJson: Language = { @@ -102,7 +102,7 @@ export const CSharpLanguageSystemTextJson: Language = { { "number-type": "decimal" }, { "any-type": "dynamic" } ], - sourceFiles: ["src/language/CSharp.ts"] + sourceFiles: ["src/language/CSharp/index.ts"] }; export const JavaLanguage: Language = { @@ -125,7 +125,7 @@ export const JavaLanguage: Language = { skipSchema: ["keyword-unions.schema"], // generates classes with names that are case-insensitively equal rendererOptions: {}, quickTestRendererOptions: [{ "array-type": "list" }], - sourceFiles: ["src/language/Java.ts"] + sourceFiles: ["src/language/Java/index.ts"] }; export const JavaLanguageWithLegacyDateTime: Language = { @@ -182,7 +182,7 @@ export const PythonLanguage: Language = { ], rendererOptions: {}, quickTestRendererOptions: [{ "python-version": "3.5" }], - sourceFiles: ["src/language/Python.ts"] + sourceFiles: ["src/language/Python/index.ts"] }; export const RustLanguage: Language = { @@ -224,7 +224,7 @@ export const RustLanguage: Language = { { visibility: "private" }, { visibility: "public" } ], - sourceFiles: ["src/language/Rust.ts"] + sourceFiles: ["src/language/Rust/index.ts"] }; export const CrystalLanguage: Language = { @@ -263,7 +263,7 @@ export const CrystalLanguage: Language = { skipMiscJSON: false, rendererOptions: {}, quickTestRendererOptions: [], - sourceFiles: ["src/language/Crystal.ts"] + sourceFiles: ["src/language/Crystal/index.ts"] }; export const RubyLanguage: Language = { @@ -405,7 +405,7 @@ export const GoLanguage: Language = { ], rendererOptions: {}, quickTestRendererOptions: [], - sourceFiles: ["src/language/Golang.ts"] + sourceFiles: ["src/language/Golang/index.ts"] }; export const CJSONLanguage: Language = { @@ -483,7 +483,7 @@ export const CJSONLanguage: Language = { ], rendererOptions: {}, quickTestRendererOptions: [{ "source-style": "single-source" }], - sourceFiles: ["src/language/CJSON.ts"] + sourceFiles: ["src/language/CJSON/index.ts"] }; export const CPlusPlusLanguage: Language = { @@ -542,7 +542,7 @@ export const CPlusPlusLanguage: Language = { { "const-style": "east-const" }, { boost: "false" } ], - sourceFiles: ["src/language/CPlusPlus.ts"] + sourceFiles: ["src/language/CPlusPlus/index.ts"] }; export const ElmLanguage: Language = { @@ -614,7 +614,7 @@ export const ElmLanguage: Language = { ], rendererOptions: {}, quickTestRendererOptions: [{ "array-type": "list" }], - sourceFiles: ["src/language/Elm.ts"] + sourceFiles: ["src/language/Elm/index.ts"] }; export const SwiftLanguage: Language = { @@ -702,7 +702,7 @@ export const SwiftLanguage: Language = { { protocol: "equatable" }, ["simple-object.json", { protocol: "hashable" }] ], - sourceFiles: ["src/language/Swift.ts"] + sourceFiles: ["src/language/Swift/index.ts"] }; export const ObjectiveCLanguage: Language = { @@ -740,7 +740,7 @@ export const ObjectiveCLanguage: Language = { skipSchema: [], rendererOptions: { functions: "true" }, quickTestRendererOptions: [], - sourceFiles: ["src/language/Objective-C.ts"] + sourceFiles: ["src/language/Objective-C/index.ts"] }; export const TypeScriptLanguage: Language = { @@ -788,7 +788,7 @@ export const TypeScriptLanguage: Language = { { converters: "all-objects" }, { readonly: "true" } ], - sourceFiles: ["src/language/TypeScript.ts"] + sourceFiles: ["src/language/TypeScript/index.ts"] }; export const JavaScriptLanguage: Language = { @@ -815,7 +815,7 @@ export const JavaScriptLanguage: Language = { { "runtime-typecheck-ignore-unknown-properties": "true" }, { converters: "top-level" } ], - sourceFiles: ["src/language/JavaScript.ts"] + sourceFiles: ["src/language/JavaScript/index.ts"] }; export const JavaScriptPropTypesLanguage: Language = { @@ -847,7 +847,7 @@ export const JavaScriptPropTypesLanguage: Language = { { "runtime-typecheck-ignore-unknown-properties": "true" }, { converters: "top-level" } ], - sourceFiles: ["src/Language/JavaScriptPropTypes.ts"] + sourceFiles: ["src/language/JavaScriptPropTypes/index.ts"] }; export const FlowLanguage: Language = { @@ -876,7 +876,7 @@ export const FlowLanguage: Language = { { "nice-property-names": "true" }, { "declare-unions": "true" } ], - sourceFiles: ["src/language/Flow.ts"] + sourceFiles: ["src/language/Flow/index.ts"] }; export const Scala3Language: Language = { @@ -956,7 +956,7 @@ I havea no idea how to encode these tests correctly. skipMiscJSON: false, rendererOptions: { framework: "circe" }, quickTestRendererOptions: [], - sourceFiles: ["src/Language/Scala3.ts"] + sourceFiles: ["src/language/Scala3/index.ts"] }; export const Smithy4sLanguage: Language = { @@ -1022,7 +1022,7 @@ I havea no idea how to encode these tests correctly. skipMiscJSON: false, rendererOptions: { framework: "just-types" }, quickTestRendererOptions: [], - sourceFiles: ["src/Language/Smithy4s.ts"] + sourceFiles: ["src/language/Smithy4s/index.ts"] }; export const KotlinLanguage: Language = { @@ -1107,7 +1107,7 @@ export const KotlinLanguage: Language = { skipMiscJSON: false, rendererOptions: {}, quickTestRendererOptions: [], - sourceFiles: ["src/Language/Kotlin.ts"] + sourceFiles: ["src/language/Kotlin/index.ts"] }; export const KotlinJacksonLanguage: Language = { @@ -1191,7 +1191,7 @@ export const KotlinJacksonLanguage: Language = { skipMiscJSON: false, rendererOptions: { framework: "jackson" }, quickTestRendererOptions: [], - sourceFiles: ["src/Language/Kotlin.ts"] + sourceFiles: ["src/language/Kotlin/index.ts"] }; export const DartLanguage: Language = { @@ -1241,7 +1241,7 @@ export const DartLanguage: Language = { skipMiscJSON: true, rendererOptions: {}, quickTestRendererOptions: [], - sourceFiles: ["src/Language/Dart.ts"] + sourceFiles: ["src/language/Dart/index.ts"] }; export const PikeLanguage: Language = { @@ -1294,7 +1294,7 @@ export const PikeLanguage: Language = { ], rendererOptions: {}, quickTestRendererOptions: [], - sourceFiles: ["src/Language/Pike.ts"] + sourceFiles: ["src/language/Pike/index.ts"] }; export const HaskellLanguage: Language = { @@ -1383,7 +1383,7 @@ export const HaskellLanguage: Language = { ], rendererOptions: {}, quickTestRendererOptions: [{ "array-type": "list" }], - sourceFiles: ["src/language/Haskell.ts"] + sourceFiles: ["src/language/Haskell/index.ts"] }; export const PHPLanguage: Language = { @@ -1401,7 +1401,7 @@ export const PHPLanguage: Language = { skipSchema: [], rendererOptions: {}, quickTestRendererOptions: [], - sourceFiles: ["src/Language/Php.ts"] + sourceFiles: ["src/language/Php/index.ts"] }; export const TypeScriptZodLanguage: Language = { @@ -1519,7 +1519,7 @@ export const TypeScriptZodLanguage: Language = { ], rendererOptions: {}, quickTestRendererOptions: [{ "array-type": "list" }], - sourceFiles: ["src/language/TypeScriptZod.ts"] + sourceFiles: ["src/language/TypeScriptZod/index.ts"] }; export const TypeScriptEffectSchemaLanguage: Language = { @@ -1635,7 +1635,7 @@ export const TypeScriptEffectSchemaLanguage: Language = { ], rendererOptions: {}, quickTestRendererOptions: [{ "array-type": "list" }], - sourceFiles: ["src/language/TypeScriptEffectSchema.ts"] + sourceFiles: ["src/language/TypeScriptEffectSchema/index.ts"] }; export const ElixirLanguage: Language = { @@ -1696,5 +1696,5 @@ export const ElixirLanguage: Language = { ], rendererOptions: {}, quickTestRendererOptions: [], - sourceFiles: ["src/language/Elixir.ts"] + sourceFiles: ["src/language/Elixir/index.ts"] };