Skip to content

Commit

Permalink
feat(serdes): Validation and Serialization according to OpenAPI schem…
Browse files Browse the repository at this point in the history
…a processed by Ajv 🎉
  • Loading branch information
gfortaine committed Jan 23, 2023
1 parent 8e01fef commit efed3b4
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 220 deletions.
6 changes: 5 additions & 1 deletion config-node.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@ additionalProperties:
files:
logger.mustache:
templateType: SupportingFiles
destinationFilename: logger.ts
destinationFilename: logger.ts
model/validation.mustache:
folder: models
templateType: SupportingFiles
destinationFilename: validation.ts
230 changes: 13 additions & 217 deletions sdk-template-overrides/typescript/model/ObjectSerializer.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> = new Set<string>([
{{#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> => 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();
Expand All @@ -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> => 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);
}
}

Expand Down Expand Up @@ -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.");
Expand All @@ -340,7 +136,7 @@ export class ObjectSerializer {
}

if (mediaType === "application/json") {
return JSON.parse(rawData);
return rawData;
}

if (mediaType === "text/html") {
Expand Down
6 changes: 4 additions & 2 deletions sdk-template-overrides/typescript/model/model.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down
Loading

0 comments on commit efed3b4

Please sign in to comment.