diff --git a/src/AccessorNames.ts b/src/AccessorNames.ts new file mode 100644 index 00000000..59be0ff3 --- /dev/null +++ b/src/AccessorNames.ts @@ -0,0 +1,139 @@ +"use strict"; + +import { Map, Set } from "immutable"; + +import { TypeAttributeKind, TypeAttributes } from "./TypeAttributes"; +import { checkStringMap, isStringMap, defined, assert } from "./Support"; +import { EnumType, ClassType, UnionType, Type } from "./Type"; + +export type AccessorEntry = string | { [language: string]: string }; + +export type AccessorNames = { [key: string]: AccessorEntry }; + +export const accessorNamesTypeAttributeKind = new TypeAttributeKind( + "accessorNames", + undefined, + _ => undefined, + undefined +); + +export function isAccessorEntry(x: any): x is AccessorEntry { + if (typeof x === "string") { + return true; + } + return isStringMap(x, (v: any): v is string => typeof v === "string"); +} + +export function checkAccessorNames(x: any): AccessorNames { + return checkStringMap(x, isAccessorEntry); +} + +// Returns [name, isFixed]. +function getFromEntry(entry: AccessorEntry, language: string): [string, boolean] | undefined { + if (typeof entry === "string") return [entry, false]; + + const maybeForLanguage = entry[language]; + if (maybeForLanguage !== undefined) return [maybeForLanguage, true]; + + const maybeWildcard = entry["*"]; + if (maybeWildcard !== undefined) return [maybeWildcard, false]; + + return undefined; +} + +function lookupKey(accessors: AccessorNames, key: string, language: string): [string, boolean] | undefined { + if (!Object.prototype.hasOwnProperty.call(accessors, key)) return undefined; + + return getFromEntry(accessors[key], language); +} + +export function classPropertyNames(c: ClassType, language: string): Map { + const accessors = accessorNamesTypeAttributeKind.tryGetInAttributes(c.getAttributes()); + const map = c.properties; + if (accessors === undefined) return map.map(_ => undefined); + return map.map((_cp, n) => lookupKey(accessors, n, language)); +} + +export function enumCaseNames(e: EnumType, language: string): Map { + const accessors = accessorNamesTypeAttributeKind.tryGetInAttributes(e.getAttributes()); + const map = e.cases.toMap(); + if (accessors === undefined) return map.map(_ => undefined); + return map.map(c => lookupKey(accessors, c, language)); +} + +export function getAccessorName( + names: Map, + original: string +): [string | undefined, boolean] { + const maybeName = names.get(original); + if (maybeName === undefined) return [undefined, false]; + return maybeName; +} + +export const unionIdentifierTypeAttributeKind = new TypeAttributeKind>( + "unionIdentifier", + (a, b) => a.union(b), + _ => undefined, + undefined +); + +let nextUnionIdentifier: number = 0; + +export function makeUnionIdentifierAttribute(): TypeAttributes { + const attributes = unionIdentifierTypeAttributeKind.makeAttributes(Set([nextUnionIdentifier])); + nextUnionIdentifier += 1; + return attributes; +} + +export const unionMemberNamesTypeAttributeKind = new TypeAttributeKind>( + "unionMemberNames", + (a, b) => a.merge(b), + _ => undefined, + undefined +); + +export function makeUnionMemberNamesAttribute(unionAttributes: TypeAttributes, entry: AccessorEntry): TypeAttributes { + const identifiers = defined(unionIdentifierTypeAttributeKind.tryGetInAttributes(unionAttributes)); + const map = identifiers.toMap().map(_ => entry); + return unionMemberNamesTypeAttributeKind.makeAttributes(map); +} + +export function unionMemberName(u: UnionType, member: Type, language: string): [string | undefined, boolean] { + const identifiers = unionIdentifierTypeAttributeKind.tryGetInAttributes(u.getAttributes()); + if (identifiers === undefined) return [undefined, false]; + + const memberNames = unionMemberNamesTypeAttributeKind.tryGetInAttributes(member.getAttributes()); + if (memberNames === undefined) return [undefined, false]; + + let names: Set = Set(); + let fixedNames: Set = Set(); + identifiers.forEach(i => { + const maybeEntry = memberNames.get(i); + if (maybeEntry === undefined) return; + const maybeName = getFromEntry(maybeEntry, language); + if (maybeName === undefined) return; + const [name, isNameFixed] = maybeName; + if (isNameFixed) { + fixedNames = fixedNames.add(name); + } else { + names = names.add(name); + } + }); + + let size: number; + let isFixed: boolean; + let first = fixedNames.first(); + if (first !== undefined) { + size = fixedNames.size; + isFixed = true; + } else { + first = names.first(); + if (first === undefined) return [undefined, false]; + + size = names.size; + isFixed = false; + } + + assert(size === 1, `More than one name given for union member: ${names.join(", ")}`); + return [first, isFixed]; +} diff --git a/src/ConvenienceRenderer.ts b/src/ConvenienceRenderer.ts index 8ca0cd58..fda31082 100644 --- a/src/ConvenienceRenderer.ts +++ b/src/ConvenienceRenderer.ts @@ -29,13 +29,19 @@ import { descriptionTypeAttributeKind, propertyDescriptionsTypeAttributeKind } from "./TypeAttributes"; +import { enumCaseNames, classPropertyNames, unionMemberName, getAccessorName } from "./AccessorNames"; const wordWrap: (s: string) => string = require("wordwrap")(90); const givenNameOrder = 1; const inferredNameOrder = 3; + const classPropertyNameOrder = 2; +const assignedClassPropertyNameOrder = 1; + const enumCaseNameOrder = 2; +const assignedEnumCaseNameOrder = 1; + const unionMemberNameOrder = 4; function splitDescription(descriptions: OrderedSet | undefined): string[] | undefined { @@ -307,8 +313,11 @@ export abstract class ConvenienceRenderer extends Renderer { c: ClassType, _className: Name, p: ClassProperty, - jsonName: string + jsonName: string, + assignedName: string | undefined ): Name | undefined { + const namer = this.namerForClassProperty(c, p); + if (namer === null) return undefined; // FIXME: This alternative should really depend on what the // actual name of the class ends up being. We can do this // with a DependencyName. @@ -319,9 +328,9 @@ export abstract class ConvenienceRenderer extends Renderer { // maybe we'll need global properties for some weird language at // some point. const alternative = `${c.getCombinedName()}_${jsonName}`; - const namer = this.namerForClassProperty(c, p); - if (namer === null) return undefined; - return new SimpleName(OrderedSet([jsonName, alternative]), namer, classPropertyNameOrder); + const order = assignedName === undefined ? classPropertyNameOrder : assignedClassPropertyNameOrder; + const names = assignedName === undefined ? [jsonName, alternative] : [assignedName]; + return new SimpleName(OrderedSet(names), namer, order); } protected makePropertyDependencyNames( @@ -342,9 +351,16 @@ export abstract class ConvenienceRenderer extends Renderer { let ns: Namespace | undefined; + const accessorNames = classPropertyNames(c, this.targetLanguage.name); const names = c.sortedProperties .map((p, jsonName) => { - const name = this.makeNameForProperty(c, className, p, jsonName); + const [assignedName, isFixed] = getAccessorName(accessorNames, jsonName); + let name: Name | undefined; + if (isFixed) { + name = new FixedName(defined(assignedName)); + } else { + name = this.makeNameForProperty(c, className, p, jsonName, assignedName); + } if (name === undefined) return undefined; if (ns === undefined) { ns = new Namespace(c.getCombinedName(), this.globalNamespace, forbiddenNamespaces, forbiddenNames); @@ -360,9 +376,14 @@ export abstract class ConvenienceRenderer extends Renderer { }; protected makeNameForUnionMember(u: UnionType, unionName: Name, t: Type): Name { - return new DependencyName(nonNull(this._unionMemberNamer), unionMemberNameOrder, lookup => - this.proposeUnionMemberName(u, unionName, t, lookup) - ); + const [assignedName, isFixed] = unionMemberName(u, t, this.targetLanguage.name); + if (isFixed) { + return new FixedName(defined(assignedName)); + } + return new DependencyName(nonNull(this._unionMemberNamer), unionMemberNameOrder, lookup => { + if (assignedName !== undefined) return assignedName; + return this.proposeUnionMemberName(u, unionName, t, lookup); + }); } private addUnionMemberNames = (u: UnionType, unionName: Name): void => { @@ -388,11 +409,18 @@ export abstract class ConvenienceRenderer extends Renderer { defined(this._memberNamesStoreView).set(u, names); }; - protected makeNameForEnumCase(e: EnumType, _enumName: Name, caseName: string): Name { + protected makeNameForEnumCase( + e: EnumType, + _enumName: Name, + caseName: string, + assignedName: string | undefined + ): Name { // FIXME: See the FIXME in `makeNameForProperty`. We do have global // enum cases, though (in Go), so this is actually useful already. const alternative = `${e.getCombinedName()}_${caseName}`; - return new SimpleName(OrderedSet([caseName, alternative]), nonNull(this._enumCaseNamer), enumCaseNameOrder); + const order = assignedName === undefined ? enumCaseNameOrder : assignedEnumCaseNameOrder; + const names = assignedName === undefined ? [caseName, alternative] : [assignedName]; + return new SimpleName(OrderedSet(names), nonNull(this._enumCaseNamer), order); } // FIXME: this is very similar to addPropertyNameds and addUnionMemberNames @@ -411,8 +439,16 @@ export abstract class ConvenienceRenderer extends Renderer { ns = new Namespace(e.getCombinedName(), this.globalNamespace, forbiddenNamespaces, forbiddenNames); } let names = Map(); + const accessorNames = enumCaseNames(e, this.targetLanguage.name); e.cases.forEach(caseName => { - names = names.set(caseName, ns.add(this.makeNameForEnumCase(e, enumName, caseName))); + const [assignedName, isFixed] = getAccessorName(accessorNames, caseName); + let name: Name; + if (isFixed) { + name = new FixedName(defined(assignedName)); + } else { + name = this.makeNameForEnumCase(e, enumName, caseName, assignedName); + } + names = names.set(caseName, ns.add(name)); }); defined(this._caseNamesStoreView).set(e, names); }; diff --git a/src/JSONSchemaInput.ts b/src/JSONSchemaInput.ts index 1b88b09c..7f13b3f2 100644 --- a/src/JSONSchemaInput.ts +++ b/src/JSONSchemaInput.ts @@ -14,7 +14,8 @@ import { defined, addHashCode, mapSync, - forEachSync + forEachSync, + checkArray } from "./Support"; import { TypeGraphBuilder, TypeRef } from "./TypeBuilder"; import { TypeNames } from "./TypeNames"; @@ -26,6 +27,13 @@ import { makeTypeAttributesInferred } from "./TypeAttributes"; import { JSONSchema, JSONSchemaStore } from "./JSONSchemaStore"; +import { + accessorNamesTypeAttributeKind, + checkAccessorNames, + makeUnionIdentifierAttribute, + isAccessorEntry, + makeUnionMemberNamesAttribute +} from "./AccessorNames"; export enum PathElementKind { Root, @@ -395,6 +403,12 @@ function makeAttributes(schema: StringMap, loc: Location, attributes: TypeAttrib }); } +function makeNonUnionAccessorAttributes(schema: StringMap): TypeAttributes | undefined { + const maybeAccessors = schema["qt-accessors"]; + if (maybeAccessors === undefined) return undefined; + return accessorNamesTypeAttributeKind.makeAttributes(checkAccessorNames(maybeAccessors)); +} + function checkTypeList(typeOrTypes: any): OrderedSet { if (typeof typeOrTypes === "string") { return OrderedSet([typeOrTypes]); @@ -591,8 +605,28 @@ export async function addTypesInSchema( } async function convertOneOrAnyOf(cases: any, kind: string): Promise { + const maybeAccessors = schema["qt-accessors"]; const unionType = typeBuilder.getUniqueUnionType(makeTypeAttributesInferred(typeAttributes), undefined); - typeBuilder.setSetOperationMembers(unionType, OrderedSet(await makeTypesFromCases(cases, kind))); + const typeRefs = await makeTypesFromCases(cases, kind); + + if (maybeAccessors !== undefined) { + const identifierAttribute = makeUnionIdentifierAttribute(); + typeBuilder.addAttributes(unionType, identifierAttribute); + + const accessors = checkArray(maybeAccessors, isAccessorEntry); + assert( + typeRefs.length === accessors.length, + `Accessor entry array must have the same number of entries as the ${kind}` + ); + for (let i = 0; i < typeRefs.length; i++) { + typeBuilder.addAttributes( + typeRefs[i], + makeUnionMemberNamesAttribute(identifierAttribute, accessors[i]) + ); + } + } + + typeBuilder.setSetOperationMembers(unionType, OrderedSet(typeRefs)); return unionType; } @@ -610,7 +644,7 @@ export async function addTypesInSchema( predicate = (x: any) => x === null; break; case "integer": - predicate = (x: any) => typeof x === "number" && x === Math.floor(x) + predicate = (x: any) => typeof x === "number" && x === Math.floor(x); break; default: predicate = (x: any) => typeof x === name; @@ -652,7 +686,7 @@ export async function addTypesInSchema( unionTypes.push(await makeArrayType()); } if (includeObject) { - unionTypes.push(...await makeObjectTypes()) + unionTypes.push(...(await makeObjectTypes())); } types.push(typeBuilder.getUniqueUnionType(inferredAttributes, OrderedSet(unionTypes))); @@ -673,6 +707,11 @@ export async function addTypesInSchema( } if (schema.oneOf !== undefined) { types.push(await convertOneOrAnyOf(schema.oneOf, "oneOf")); + } else { + const maybeAttributes = makeNonUnionAccessorAttributes(schema); + if (maybeAttributes !== undefined) { + typeBuilder.addAttributes(intersectionType, maybeAttributes); + } } if (schema.anyOf !== undefined) { types.push(await convertOneOrAnyOf(schema.anyOf, "anyOf")); diff --git a/src/Language/CPlusPlus.ts b/src/Language/CPlusPlus.ts index 1227431a..2c9c7b80 100644 --- a/src/Language/CPlusPlus.ts +++ b/src/Language/CPlusPlus.ts @@ -78,6 +78,7 @@ export default class CPlusPlusTargetLanguage extends TargetLanguage { } protected get rendererClass(): new ( + targetLanguage: TargetLanguage, graph: TypeGraph, leadingComments: string[] | undefined, ...optionValues: any[] @@ -254,6 +255,7 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { private readonly _caseNamingFunction: Namer; constructor( + targetLanguage: TargetLanguage, graph: TypeGraph, leadingComments: string[] | undefined, private readonly _justTypes: boolean, @@ -262,7 +264,7 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { _memberNamingStyle: NamingStyle, _enumeratorNamingStyle: NamingStyle ) { - super(graph, leadingComments); + super(targetLanguage, graph, leadingComments); this._namespaceNames = List(namespaceName.split("::")); diff --git a/src/Language/CSharp.ts b/src/Language/CSharp.ts index 1c7f4381..35ee89d1 100644 --- a/src/Language/CSharp.ts +++ b/src/Language/CSharp.ts @@ -75,6 +75,7 @@ export default class CSharpTargetLanguage extends TargetLanguage { } protected get rendererClass(): new ( + targetLanguage: TargetLanguage, graph: TypeGraph, leadingComments: string[] | undefined, ...optionValues: any[] @@ -128,6 +129,7 @@ export class CSharpRenderer extends ConvenienceRenderer { protected readonly needAttributes: boolean; constructor( + targetLanguage: TargetLanguage, graph: TypeGraph, leadingComments: string[] | undefined, protected readonly namespaceName: string, @@ -136,7 +138,7 @@ export class CSharpRenderer extends ConvenienceRenderer { private readonly _useList: boolean, outputFeatures: OutputFeatures ) { - super(graph, leadingComments); + super(targetLanguage, graph, leadingComments); this.needHelpers = outputFeatures.helpers; this.needAttributes = outputFeatures.attributes; } diff --git a/src/Language/Elm.ts b/src/Language/Elm.ts index cb97a0b7..98f402ac 100644 --- a/src/Language/Elm.ts +++ b/src/Language/Elm.ts @@ -51,6 +51,7 @@ export default class ElmTargetLanguage extends TargetLanguage { } protected get rendererClass(): new ( + targetLanguage: TargetLanguage, graph: TypeGraph, leadingComments: string[] | undefined, ...optionValues: any[] @@ -153,13 +154,14 @@ export class ElmRenderer extends ConvenienceRenderer { private _namedTypeDependents: Map = Map(); constructor( + targetLanguage: TargetLanguage, graph: TypeGraph, leadingComments: string[] | undefined, private readonly _justTypes: boolean, private readonly _moduleName: string, private readonly _useList: boolean ) { - super(graph, leadingComments); + super(targetLanguage, graph, leadingComments); } protected forbiddenNamesForGlobalNamespace(): string[] { diff --git a/src/Language/Golang.ts b/src/Language/Golang.ts index b4d734f0..bbc60e5d 100644 --- a/src/Language/Golang.ts +++ b/src/Language/Golang.ts @@ -49,6 +49,7 @@ export default class GoTargetLanguage extends TargetLanguage { } protected get rendererClass(): new ( + targetLanguage: TargetLanguage, graph: TypeGraph, leadingComments: string[] | undefined, ...optionValues: any[] @@ -96,12 +97,13 @@ export class GoRenderer extends ConvenienceRenderer { private _topLevelUnmarshalNames = Map(); constructor( + targetLanguage: TargetLanguage, graph: TypeGraph, leadingComments: string[] | undefined, private readonly _justTypes: boolean, private readonly _packageName: string ) { - super(graph, leadingComments); + super(targetLanguage, graph, leadingComments); } protected topLevelNameStyle(rawName: string): string { diff --git a/src/Language/JSONSchema.ts b/src/Language/JSONSchema.ts index 6e216eac..6c42694f 100644 --- a/src/Language/JSONSchema.ts +++ b/src/Language/JSONSchema.ts @@ -31,6 +31,7 @@ export default class JSONSchemaTargetLanguage extends TargetLanguage { } protected get rendererClass(): new ( + targetLanguage: TargetLanguage, graph: TypeGraph, leadingComments: string[] | undefined, ...optionValues: any[] diff --git a/src/Language/Java.ts b/src/Language/Java.ts index 173e7dbb..16f141f1 100644 --- a/src/Language/Java.ts +++ b/src/Language/Java.ts @@ -58,6 +58,7 @@ export class JavaTargetLanguage extends TargetLanguage { } protected get rendererClass(): new ( + targetLanguage: TargetLanguage, graph: TypeGraph, leadingComments: string[] | undefined, ...optionValues: any[] @@ -183,12 +184,13 @@ export class JavaRenderer extends ConvenienceRenderer { private _gettersAndSettersForPropertyName: Map; constructor( + targetLanguage: TargetLanguage, graph: TypeGraph, leadingComments: string[] | undefined, private readonly _packageName: string, private readonly _justTypes: boolean ) { - super(graph, leadingComments); + super(targetLanguage, graph, leadingComments); this._gettersAndSettersForPropertyName = Map(); } diff --git a/src/Language/JavaScript.ts b/src/Language/JavaScript.ts index d3695bd3..a3a971d3 100644 --- a/src/Language/JavaScript.ts +++ b/src/Language/JavaScript.ts @@ -1,6 +1,6 @@ "use strict"; -import { Type, matchType, directlyReachableSingleNamedType, ClassProperty } from "../Type"; +import { Type, matchType, directlyReachableSingleNamedType, ClassProperty, ClassType } from "../Type"; import { TypeGraph } from "../TypeGraph"; import { utf16LegalizeCharacters, @@ -45,6 +45,7 @@ export class JavaScriptTargetLanguage extends TargetLanguage { } protected get rendererClass(): new ( + targetLanguage: TargetLanguage, graph: TypeGraph, leadingComments: string[] | undefined, ...optionValues: any[] @@ -96,8 +97,13 @@ function propertyNameStyle(original: string): string { } export class JavaScriptRenderer extends ConvenienceRenderer { - constructor(graph: TypeGraph, leadingComments: string[] | undefined, private readonly _runtimeTypecheck: boolean) { - super(graph, leadingComments); + constructor( + targetLanguage: TargetLanguage, + graph: TypeGraph, + leadingComments: string[] | undefined, + private readonly _runtimeTypecheck: boolean + ) { + super(targetLanguage, graph, leadingComments); } protected topLevelNameStyle(rawName: string): string { @@ -124,6 +130,17 @@ export class JavaScriptRenderer extends ConvenienceRenderer { return directlyReachableSingleNamedType(type); } + protected makeNameForProperty( + c: ClassType, + className: Name, + p: ClassProperty, + jsonName: string, + _assignedName: string | undefined + ): Name | undefined { + // Ignore the assigned name + return super.makeNameForProperty(c, className, p, jsonName, undefined); + } + protected emitDescriptionBlock(lines: string[]): void { this.emitCommentLines(lines, " * ", "/**", " */"); } diff --git a/src/Language/Objective-C.ts b/src/Language/Objective-C.ts index 7ddd51a8..44558336 100644 --- a/src/Language/Objective-C.ts +++ b/src/Language/Objective-C.ts @@ -72,6 +72,7 @@ export default class ObjectiveCTargetLanguage extends TargetLanguage { } protected get rendererClass(): new ( + targetLanguage: TargetLanguage, graph: TypeGraph, leadingComments: string[] | undefined, ...optionValues: any[] @@ -248,6 +249,7 @@ export class ObjectiveCRenderer extends ConvenienceRenderer { pseudoEnums: Set; constructor( + targetLanguage: TargetLanguage, graph: TypeGraph, leadingComments: string[] | undefined, private readonly _justTypes: boolean, @@ -256,7 +258,7 @@ export class ObjectiveCRenderer extends ConvenienceRenderer { private readonly _extraComments: boolean, private readonly _marshalingFunctions: boolean ) { - super(graph, leadingComments); + super(targetLanguage, graph, leadingComments); // Infer the class prefix from a top-level name if it's not given if (this._classPrefix === DEFAULT_CLASS_PREFIX) { diff --git a/src/Language/Ruby/index.ts b/src/Language/Ruby/index.ts index e25728d2..12a33def 100644 --- a/src/Language/Ruby/index.ts +++ b/src/Language/Ruby/index.ts @@ -75,6 +75,7 @@ export default class RubyTargetLanguage extends TargetLanguage { } protected get rendererClass(): new ( + targetLanguage: TargetLanguage, graph: TypeGraph, leadingComments: string[] | undefined, ...optionValues: any[] @@ -124,12 +125,13 @@ function memberNameStyle(original: string): string { export class RubyRenderer extends ConvenienceRenderer { constructor( + targetLanguage: TargetLanguage, graph: TypeGraph, leadingComments: string[] | undefined, private readonly _justTypes: boolean, private readonly _strictness: Strictness ) { - super(graph, leadingComments); + super(targetLanguage, graph, leadingComments); } protected get commentLineStart(): string { diff --git a/src/Language/Rust.ts b/src/Language/Rust.ts index 1b44e117..63f71b34 100644 --- a/src/Language/Rust.ts +++ b/src/Language/Rust.ts @@ -47,7 +47,11 @@ export default class RustTargetLanguage extends TargetLanguage { private readonly _deriveDebugOption = new BooleanOption("derive-debug", "Derive Debug impl", false); - protected get rendererClass(): new (graph: TypeGraph, ...optionValues: any[]) => ConvenienceRenderer { + protected get rendererClass(): new ( + targetLanguage: TargetLanguage, + graph: TypeGraph, + ...optionValues: any[] + ) => ConvenienceRenderer { return RustRenderer; } @@ -182,13 +186,14 @@ const rustStringEscape = utf32ConcatMap(escapeNonPrintableMapper(isPrintable, st export class RustRenderer extends ConvenienceRenderer { constructor( + targetLanguage: TargetLanguage, graph: TypeGraph, leadingComments: string[] | undefined, private readonly _density: Density, private readonly _visibility: Visibility, private readonly _deriveDebug: boolean ) { - super(graph, leadingComments); + super(targetLanguage, graph, leadingComments); } protected makeNamedTypeNamer(): Namer { diff --git a/src/Language/Swift.ts b/src/Language/Swift.ts index 3e139675..65438b07 100644 --- a/src/Language/Swift.ts +++ b/src/Language/Swift.ts @@ -84,6 +84,7 @@ export default class SwiftTargetLanguage extends TargetLanguage { } protected get rendererClass(): new ( + targetLanguage: TargetLanguage, graph: TypeGraph, leadingComments: string[] | undefined, ...optionValues: any[] @@ -229,6 +230,7 @@ export class SwiftRenderer extends ConvenienceRenderer { private _needNull: boolean = false; constructor( + targetLanguage: TargetLanguage, graph: TypeGraph, leadingComments: string[] | undefined, private readonly _justTypes: boolean, @@ -238,7 +240,7 @@ export class SwiftRenderer extends ConvenienceRenderer { private readonly _convenienceInitializers: boolean, private readonly _alamofire: boolean ) { - super(graph, leadingComments); + super(targetLanguage, graph, leadingComments); } protected forbiddenNamesForGlobalNamespace(): string[] { diff --git a/src/Language/TypeScriptFlow.ts b/src/Language/TypeScriptFlow.ts index 4d72d46e..11c88ab5 100644 --- a/src/Language/TypeScriptFlow.ts +++ b/src/Language/TypeScriptFlow.ts @@ -10,6 +10,7 @@ import { ConvenienceRenderer } from "../ConvenienceRenderer"; import { BooleanOption, Option } from "../RendererOptions"; import { JavaScriptTargetLanguage, JavaScriptRenderer } from "./JavaScript"; import { defined } from "../Support"; +import { TargetLanguage } from "../TargetLanguage"; export abstract class TypeScriptFlowBaseTargetLanguage extends JavaScriptTargetLanguage { private readonly _justTypes = new BooleanOption("just-types", "Interfaces only", false); @@ -20,6 +21,7 @@ export abstract class TypeScriptFlowBaseTargetLanguage extends JavaScriptTargetL } protected abstract get rendererClass(): new ( + targetLanguage: TargetLanguage, graph: TypeGraph, leadingComments: string[] | undefined, ...optionValues: any[] @@ -32,6 +34,7 @@ export class TypeScriptTargetLanguage extends TypeScriptFlowBaseTargetLanguage { } protected get rendererClass(): new ( + targetLanguage: TargetLanguage, graph: TypeGraph, leadingComments: string[] | undefined, ...optionValues: any[] @@ -44,13 +47,14 @@ export abstract class TypeScriptFlowBaseRenderer extends JavaScriptRenderer { private readonly _inlineUnions: boolean; constructor( + targetLanguage: TargetLanguage, graph: TypeGraph, leadingComments: string[] | undefined, private readonly _justTypes: boolean, declareUnions: boolean, runtimeTypecheck: boolean ) { - super(graph, leadingComments, runtimeTypecheck); + super(targetLanguage, graph, leadingComments, runtimeTypecheck); this._inlineUnions = !declareUnions; } @@ -214,6 +218,7 @@ export class FlowTargetLanguage extends TypeScriptFlowBaseTargetLanguage { } protected get rendererClass(): new ( + targetLanguage: TargetLanguage, graph: TypeGraph, leadingComments: string[] | undefined, ...optionValues: any[] diff --git a/src/Renderer.ts b/src/Renderer.ts index a209fdb2..46a03944 100644 --- a/src/Renderer.ts +++ b/src/Renderer.ts @@ -8,6 +8,7 @@ import { Name, Namespace, assignNames } from "./Naming"; import { Source, Sourcelike, NewlineSource, annotated, sourcelikeToSource, newline } from "./Source"; import { AnnotationData, IssueAnnotationData } from "./Annotation"; import { assert, panic, StringMap } from "./Support"; +import { TargetLanguage } from "./TargetLanguage"; export type RenderResult = { sources: OrderedMap; @@ -46,7 +47,7 @@ export abstract class Renderer { // @ts-ignore: Initialized in startEmit, which is called from the constructor private _preventBlankLine: boolean; - constructor(protected readonly typeGraph: TypeGraph, protected readonly leadingComments: string[] | undefined) { + constructor(protected readonly targetLanguage: TargetLanguage, protected readonly typeGraph: TypeGraph, protected readonly leadingComments: string[] | undefined) { this._finishedFiles = Map(); this.startEmit(); } diff --git a/src/Support.ts b/src/Support.ts index 542b13e7..9c84a5fd 100644 --- a/src/Support.ts +++ b/src/Support.ts @@ -17,17 +17,43 @@ export function setUnion>(a: TSet, b: TSet): TSet { export type StringMap = { [name: string]: any }; -export function checkStringMap(x: any): StringMap { - if (typeof x !== "object") { - return panic(`Value must be an object, but is ${x}`); +export function isStringMap(x: any): x is StringMap; +export function isStringMap(x: any, checkValue: (v: any) => v is T): x is { [name: string]: T }; +export function isStringMap(x: any, checkValue?: (v: any) => v is T): boolean { + if (typeof x !== "object" || Array.isArray(x) || x === null) { + return false; } - return x; + if (checkValue !== undefined) { + for (const k of Object.getOwnPropertyNames(x)) { + const v = x[k]; + if (!checkValue(v)) { + return false; + } + } + } + return true; } -export function checkArray(x: any): any[] { +export function checkStringMap(x: any): StringMap; +export function checkStringMap(x: any, checkValue: (v: any) => v is T): { [name: string]: T }; +export function checkStringMap(x: any, checkValue?: (v: any) => v is T): StringMap { + if (isStringMap(x, checkValue as any)) return x; + return panic(`Value must be an object, but is ${x}`); +} + +export function checkArray(x: any): any[]; +export function checkArray(x: any, checkItem: (v: any) => v is T): T[]; +export function checkArray(x: any, checkItem?: (v: any) => v is T): T[] { if (!Array.isArray(x)) { return panic(`Value must be an array, but is ${x}`); } + if (checkItem !== undefined) { + for (const v of x) { + if (!checkItem(v)) { + return panic(`Array item does not satisfy constraint: ${v}`); + } + } + } return x; } diff --git a/src/TargetLanguage.ts b/src/TargetLanguage.ts index 356a2efa..3c8fd02f 100644 --- a/src/TargetLanguage.ts +++ b/src/TargetLanguage.ts @@ -29,7 +29,12 @@ export abstract class TargetLanguage { return { actual, display }; } + get name(): string { + return defined(this.names[0]); + } + protected abstract get rendererClass(): new ( + targetLanguage: TargetLanguage, graph: TypeGraph, leadingComments: string[] | undefined, ...optionValues: any[] @@ -41,6 +46,7 @@ export abstract class TargetLanguage { rendererOptions: { [name: string]: any } ): Renderer { return new this.rendererClass( + this, graph, leadingComments, ...this.getOptions().map(o => o.getValue(rendererOptions)) diff --git a/src/TypeAttributes.ts b/src/TypeAttributes.ts index ff843c85..14aab449 100644 --- a/src/TypeAttributes.ts +++ b/src/TypeAttributes.ts @@ -5,10 +5,10 @@ import { Map, OrderedSet, hash } from "immutable"; import { panic, setUnion } from "./Support"; export class TypeAttributeKind { public readonly combine: (a: T, b: T) => T; - public readonly makeInferred: (a: T) => T; + public readonly makeInferred: (a: T) => (T | undefined); public readonly stringify: (a: T) => string | undefined; - constructor(readonly name: string, combine: ((a: T, b: T) => T) | undefined, makeInferred: ((a: T) => T) | undefined, stringify: ((a: T) => string | undefined) | undefined) { + constructor(readonly name: string, combine: ((a: T, b: T) => T) | undefined, makeInferred: ((a: T) => (T | undefined)) | undefined, stringify: ((a: T) => string | undefined) | undefined) { if (combine === undefined) { combine = () => { return panic(`Cannot combine type attribute ${name}`); @@ -93,7 +93,7 @@ export function combineTypeAttributes(firstOrArray: TypeAttributes[] | TypeAttri } export function makeTypeAttributesInferred(attr: TypeAttributes): TypeAttributes { - return attr.map((value, kind) => kind.makeInferred(value)); + return attr.map((value, kind) => kind.makeInferred(value)).filter(v => v !== undefined); } export const descriptionTypeAttributeKind = new TypeAttributeKind>("description", setUnion, a => a, undefined); diff --git a/test/inputs/schema/accessors.1.json b/test/inputs/schema/accessors.1.json new file mode 100644 index 00000000..de651fbf --- /dev/null +++ b/test/inputs/schema/accessors.1.json @@ -0,0 +1,6 @@ +{ + "union": 123, + "enum": "red", + "foo": "FOO!", + "bar": "BAAAAR" +} \ No newline at end of file diff --git a/test/inputs/schema/accessors.schema b/test/inputs/schema/accessors.schema new file mode 100644 index 00000000..9d42bfdf --- /dev/null +++ b/test/inputs/schema/accessors.schema @@ -0,0 +1,44 @@ +{ + "type": "object", + "properties": { + "union": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "number" + } + ], + "qt-accessors": ["presence", "amount"] + }, + "enum": { + "type": "string", + "enum": ["red", "green", "blue"], + "qt-accessors": { + "red": "fire", + "green": "grass" + } + }, + "foo": { + "type": "string" + }, + "bar": { + "type": "string" + } + }, + "qt-accessors": { + "union": "unionization", + "enum": "enumerification", + "foo": { + "cs": "Fu_uu", + "java": "Goo" + }, + "bar": { + "go": "Bah", + "*": "barre" + } + }, + "required": ["union", "enum", "foo", "bar"], + "additionalProperties": false +}