diff --git a/config-node.yaml b/config-node.yaml index d9c20997..917d45c5 100644 --- a/config-node.yaml +++ b/config-node.yaml @@ -7,4 +7,8 @@ additionalProperties: files: logger.mustache: templateType: SupportingFiles - destinationFilename: logger.ts \ No newline at end of file + destinationFilename: logger.ts + model/validation.mustache: + folder: models + templateType: SupportingFiles + destinationFilename: validation.ts \ No newline at end of file diff --git a/sdk-template-overrides/typescript/model/ObjectSerializer.mustache b/sdk-template-overrides/typescript/model/ObjectSerializer.mustache index 10508692..b45bb18b 100644 --- a/sdk-template-overrides/typescript/model/ObjectSerializer.mustache +++ b/sdk-template-overrides/typescript/model/ObjectSerializer.mustache @@ -4,151 +4,37 @@ export * from '{{{ importPath }}}{{extensionForDeno}}'; {{/model}} {{/models}} -{{#models}} -{{#model}} -import { {{classname}}{{#hasEnums}}{{#vars}}{{#isEnum}}, {{classname}}{{enumName}} {{/isEnum}} {{/vars}}{{/hasEnums}} } from '{{{ importPath }}}{{extensionForDeno}}'; -{{/model}} -{{/models}} import { dateFromRFC3339String, dateToRFC3339String, UnparsedObject } from "../util{{extensionForDeno}}"; -import { logger } from "../logger{{extensionForDeno}}"; +import { validateAndSerialize, parseAndLog, enumsMap } from "./validation{{extensionForDeno}}"; /* tslint:disable:no-unused-variable */ const primitives = [ "string", "boolean", - "double", - "integer", - "long", - "float", "number", - "any" + "object" ]; -const ARRAY_PREFIX = "Array<"; -const MAP_PREFIX = "{ [key: string]: "; -const TUPLE_PREFIX = "["; - const supportedMediaTypes: { [mediaType: string]: number } = { "application/json": Infinity, "application/octet-stream": 0, "application/x-www-form-urlencoded": 0 } - -let enumsMap: Set = new Set([ - {{#models}} - {{#model}} - {{#isEnum}} - "{{classname}}{{enumName}}", - {{/isEnum}} - {{#hasEnums}} - {{#vars}} - {{#isEnum}} - "{{classname}}{{enumName}}", - {{/isEnum}} - {{/vars}} - {{/hasEnums}} - {{/model}} - {{/models}} -]); - -let typeMap: {[index: string]: any} = { - {{#models}} - {{#model}} - {{^isEnum}} - "{{classname}}": {{classname}}, - {{/isEnum}} - {{/model}} - {{/models}} -} - -let oneOfMap: {[index: string]: string[]} = { - {{#models}} - {{#model}} - {{#oneOf}} - {{#-first}} - "{{#lambda.pascalcase}}{{name}}{{/lambda.pascalcase}}": [{{#oneOf}}{{{#dataType}}}"{{{.}}}"{{^-last}}, {{/-last}}{{{/dataType}}}{{/oneOf}}], - {{/-first}} - {{/oneOf}} - {{/model}} - {{/models}} -}; - export class ObjectSerializer { - public static findCorrectType(data: any, expectedType: string) { - if (data == undefined) { - return expectedType; - } else if (primitives.indexOf(expectedType.toLowerCase()) !== -1) { - return expectedType; - } else if (expectedType === "Date") { - return expectedType; - } else { - if (enumsMap.has(expectedType)) { - return expectedType; - } - - if (!typeMap[expectedType]) { - return expectedType; // w/e we don't know the type - } - - // Check the discriminator - let discriminatorProperty = typeMap[expectedType].discriminator; - if (discriminatorProperty == null) { - return expectedType; // the type does not have a discriminator. use it. - } else { - if (data[discriminatorProperty]) { - var discriminatorType = data[discriminatorProperty]; - if(typeMap[discriminatorType]){ - return discriminatorType; // use the type given in the discriminator - } else { - return expectedType; // discriminator did not map to a type - } - } else { - return expectedType; // discriminator was not present (or an empty string) - } - } - } - } - public static serialize(data: any, type: string, format: string) { - if (data == undefined || type == "any") { + if (data == undefined || type === "any") { return data; } else if (data instanceof UnparsedObject) { return data._data; - } else if (primitives.includes(type.toLowerCase()) && typeof data == type.toLowerCase()) { - return data; - } else if (type.startsWith(ARRAY_PREFIX)) { - if (!Array.isArray(data)) { - throw new TypeError(`mismatch types '${data}' and '${type}'`); - } - // Array => Type - const subType: string = type.substring(ARRAY_PREFIX.length, type.length - 1); - const transformedData: any[] = []; - for (const element of data) { - transformedData.push(ObjectSerializer.serialize(element, subType, format)); - } - return transformedData; - } else if (type.startsWith(TUPLE_PREFIX)) { - // We only support homegeneus tuples - const subType: string = type.substring(TUPLE_PREFIX.length, type.length - 1).split(", ")[0]; - const transformedData: any[] = []; - for (const element of data) { - transformedData.push(ObjectSerializer.serialize(element, subType, format)); - } - return transformedData; - } else if (type.startsWith(MAP_PREFIX)) { - // { [key: string]: Type; } => Type - const subType: string = type.substring(MAP_PREFIX.length, type.length - 3); - const transformedData: { [key: string]: any } = {}; - for (const key in data) { - transformedData[key] = ObjectSerializer.serialize(data[key], subType, format); - } - return transformedData; + } else if (primitives.includes(type.toLowerCase())) { + if (typeof data === type.toLowerCase()) return data; + throw new TypeError(`mismatch types '${data}' and '${type}'`); } else if (type === "Date") { - if ("string" == typeof data) { + if ("string" === typeof data) { return data; } - if (format == "date" || format == "date-time") { + if (format === "date" || format === "date-time") { return dateToRFC3339String(data) } else { return data.toISOString(); @@ -157,115 +43,25 @@ export class ObjectSerializer { if (enumsMap.has(type)) { return data; } - if (oneOfMap[type]) { - const oneOfs: any[] = []; - for (const oneOf of oneOfMap[type]) { - try { - oneOfs.push(ObjectSerializer.serialize(data, oneOf, format)); - } catch (e) { - logger.debug(`could not serialize ${oneOf} (${e})`) - } - } - if (oneOfs.length > 1) { - throw new TypeError(`${data} matches multiple types from ${oneOfMap[type]} ${oneOfs}`); - } - if (oneOfs.length == 0) { - throw new TypeError(`${data} doesn't match any type from ${oneOfMap[type]} ${oneOfs}`); - } - return oneOfs[0]; - } - if (!typeMap[type]) { // in case we dont know the type - return data; - } - - // Get the actual type of this object - type = this.findCorrectType(data, type); - - // get the map for the correct type. - let attributeTypes = typeMap[type].getAttributeTypeMap(); - let instance: {[index: string]: any} = {}; - for (let index in attributeTypes) { - let attributeType = attributeTypes[index]; - instance[attributeType.baseName] = ObjectSerializer.serialize(data[attributeType.baseName], attributeType.type, attributeType.format); - } - return instance; + return validateAndSerialize(data, type); } } public static deserialize(data: any, type: string, format: string) { // polymorphism may change the actual type. - type = ObjectSerializer.findCorrectType(data, type); if (data == undefined || type == "any") { return data; } else if (primitives.includes(type.toLowerCase()) && typeof data == type.toLowerCase()) { return data; - } else if (type.startsWith(ARRAY_PREFIX)) { - // Assert the passed data is Array type - if (!Array.isArray(data)) { - throw new TypeError(`mismatch types '${data}' and '${type}'`); - } - // Array => Type - const subType: string = type.substring(ARRAY_PREFIX.length, type.length - 1); - const transformedData: any[] = []; - for (const element of data) { - transformedData.push(ObjectSerializer.deserialize(element, subType, format)); - } - return transformedData; - } else if (type.startsWith(TUPLE_PREFIX)) { - // [Type,...] => Type - const subType: string = type.substring(TUPLE_PREFIX.length, type.length - 1).split(", ")[0]; - const transformedData: any[] = []; - for (const element of data) { - transformedData.push(ObjectSerializer.deserialize(element, subType, format)); - } - return transformedData; - } else if (type.startsWith(MAP_PREFIX)) { - // { [key: string]: Type; } => Type - const subType: string = type.substring(MAP_PREFIX.length, type.length - 3); - const transformedData: { [key: string]: any } = {}; - for (const key in data) { - transformedData[key] = ObjectSerializer.deserialize(data[key], subType, format); - } - return transformedData; } else if (type === "Date") { return dateFromRFC3339String(data) } else { if (enumsMap.has(type)) {// is Enum return data; } - if (oneOfMap[type]) { - const oneOfs: any[] = []; - for (const oneOf of oneOfMap[type]) { - try { - const d = ObjectSerializer.deserialize(data, oneOf, format); - if (!d?._unparsed) { - oneOfs.push(d); - } - } catch (e) { - logger.debug(`could not deserialize ${oneOf} (${e})`) - } - - } - if (oneOfs.length != 1) { - return new UnparsedObject(data); - } - return oneOfs[0]; - } - - if (!typeMap[type]) { // dont know the type - return data; - } - let instance = new typeMap[type](); - let attributeTypes = typeMap[type].getAttributeTypeMap(); - for (let index in attributeTypes) { - let attributeType = attributeTypes[index]; - let value = ObjectSerializer.deserialize(data[attributeType.baseName], attributeType.type, attributeType.format); - if (value !== undefined) { - instance[attributeType.name] = value; - } - } - return instance; + + return parseAndLog(data, type); } } @@ -321,7 +117,7 @@ export class ObjectSerializer { } if (mediaType === "application/json") { - return JSON.stringify(data); + return String(data); } throw new Error("The mediaType " + mediaType + " is not supported by ObjectSerializer.stringify."); @@ -340,7 +136,7 @@ export class ObjectSerializer { } if (mediaType === "application/json") { - return JSON.parse(rawData); + return rawData; } if (mediaType === "text/html") { diff --git a/sdk-template-overrides/typescript/model/model.mustache b/sdk-template-overrides/typescript/model/model.mustache index 99cce201..e46f1a51 100644 --- a/sdk-template-overrides/typescript/model/model.mustache +++ b/sdk-template-overrides/typescript/model/model.mustache @@ -30,13 +30,15 @@ export class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{ {{/discriminator}} {{^isArray}} - static readonly attributeTypeMap: Array<{name: string, baseName: string, type: string, format: string}> = [ + static readonly attributeTypeMap: Array<{name: string, baseName: string, type: string, format: string, required: boolean, nullable: boolean}> = [ {{#vars}} { "name": "{{name}}", "baseName": "{{baseName}}", "type": "{{#isEnum}}{{{datatypeWithEnum}}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}", - "format": "{{dataFormat}}" + "format": "{{dataFormat}}", + "required": {{required}}, + "nullable": {{isNullable}} }{{^-last}}, {{/-last}} {{/vars}} diff --git a/sdk-template-overrides/typescript/model/validation.mustache b/sdk-template-overrides/typescript/model/validation.mustache new file mode 100644 index 00000000..17ce6dd7 --- /dev/null +++ b/sdk-template-overrides/typescript/model/validation.mustache @@ -0,0 +1,161 @@ +import Ajv, { JTDSchemaType } from "ajv/dist/jtd.js"; +import { logger } from "../logger{{extensionForDeno}}"; + +{{#models}} +{{#model}} +import { {{classname}}{{#hasEnums}}{{#vars}}{{#isEnum}}, {{classname}}{{enumName}} {{/isEnum}} {{/vars}}{{/hasEnums}} } from '{{{ importPath }}}{{extensionForDeno}}'; +{{/model}} +{{/models}} + +export const ajv = new Ajv(); + +export const ARRAY_PREFIX = "Array<"; +export const MAP_PREFIX = "{ [key: string]: "; +export const TUPLE_PREFIX = "["; + +export const enumsMap: Set = new Set([ + {{#models}} + {{#model}} + {{#isEnum}} + "{{classname}}{{enumName}}", + {{/isEnum}} + {{#hasEnums}} + {{#vars}} + {{#isEnum}} + "{{classname}}{{enumName}}", + {{/isEnum}} + {{/vars}} + {{/hasEnums}} + {{/model}} + {{/models}} +]); + +const typeMap: {[index: string]: any} = { + {{#models}} + {{#model}} + {{^isEnum}} + "{{classname}}": {{classname}}, + {{/isEnum}} + {{/model}} + {{/models}} +} + +const oneOfMap: {[index: string]: string[]} = { + {{#models}} + {{#model}} + {{#oneOf}} + {{#-first}} + "{{#lambda.pascalcase}}{{name}}{{/lambda.pascalcase}}": [{{#oneOf}}"{{{.}}}"{{^-last}}, {{/-last}}{{/oneOf}}], + {{/-first}} + {{/oneOf}} + {{/model}} + {{/models}} +}; + +function typeJTD(type: string, nullable?: boolean): any { + switch (type) { + case "any": + return {}; + case "number": + return { type: "float64", ...(nullable && { nullable: true }) }; + default: { + if (type.startsWith(ARRAY_PREFIX)) { + const subType = type.substring(ARRAY_PREFIX.length, type.length - 1); + const elements = typeJTD(subType); + + return { elements }; + } else if (enumsMap.has(type)) { + return {}; + } else if (oneOfMap[type]) { + const instance: { metadata: { union: unknown[] }} = { metadata: { union: [] } }; + + for (const t of oneOfMap[type]) { + let form; + if (t.startsWith(ARRAY_PREFIX)) { + // Array => Type + const subType = t.substring( + ARRAY_PREFIX.length, + t.length - 1 + ); + const elements = typeJTD(subType); + + form = { elements }; + } else { + form = typeJTD(t); + } + + (instance?.metadata?.union as unknown[]).push(form); + } + + return instance; + } else if (typeMap[type]) { + let instance = { ...(nullable && { nullable: true }) }; + const attributeTypes = typeMap[type].getAttributeTypeMap(); + + for (const index in attributeTypes) { + const attributeType = attributeTypes[index]; + + instance = { + ...instance, + [attributeType.required ? "properties" : "optionalProperties"]: { + ...(instance as any)[attributeType.required ? "properties" : "optionalProperties"], + [attributeType.baseName]: typeJTD(attributeType.type, attributeType.nullable), + }, + } + } + + return instance; + } + + return { type, ...(nullable && { nullable: true }) }; + } + } +} + +const schemaMap: {[index: string]: JTDSchemaType} = {}; + +for (const key in typeMap) { + // get the map for the correct type. + const attributeTypes = typeMap[key].getAttributeTypeMap(); + schemaMap[key] = {}; + + for (const index in attributeTypes) { + const attributeType = attributeTypes[index]; + + schemaMap[key] = { + ...schemaMap[key], + [attributeType.required ? "properties" : "optionalProperties"]: { + ...(schemaMap[key] as any)[attributeType.required ? "properties" : "optionalProperties"], + [attributeType.baseName]: typeJTD(attributeType.type, attributeType.nullable), + }, + } as any; + } + + ajv.addSchema(schemaMap[key], key); +} + +export function validateAndSerialize(data: any, type: string) { + const validate = ajv.getSchema(type); + const valid = validate?.(data); + + if (valid) { + const serialize = ajv.compileSerializer(schemaMap[type]); + + return serialize(data); + } else { + logger.debug(validate?.errors); + throw new TypeError(`mismatch types '${data}' and '${type}'`); + } +} + +export function parseAndLog(json: any, type: string) { + const parse = ajv.compileParser(schemaMap[type]); + const data = parse(json); + + if (data === undefined) { + logger.debug(parse.message); // error message from the last parse call + logger.debug(parse.position); // error position in string + } else { + return data; + } +} \ No newline at end of file diff --git a/sdk-template-overrides/typescript/package.mustache b/sdk-template-overrides/typescript/package.mustache index 5f712f90..b79c1e44 100644 --- a/sdk-template-overrides/typescript/package.mustache +++ b/sdk-template-overrides/typescript/package.mustache @@ -33,6 +33,7 @@ }, "dependencies": { "@fortaine/fetch": "^6.2.3", + "ajv": "^8.12.0", {{#frameworks}} {{#fetch-api}} {{#platforms}}