diff --git a/src/index.ts b/src/index.ts index 3c65f66..1dd463a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,8 @@ function toTypeScriptType(property: Property | Parameter): string { let tp if (property.$ref) { tp = property.$ref.name + } else if (property.additionalProperties) { + tp = `Record` } else { switch (property.type) { case "string": @@ -44,11 +46,143 @@ function toTypeScriptType(property: Property | Parameter): string { } } - if (!tp) throw new Error("Cant convert property to typescript type: " + property.type) + if (!tp) throw new Error("Cant convert property to typescript type: " + JSON.stringify(property)) if (!property.nullable) return tp return `${tp} | null` } +function getBaseRef(property: Property): string { + if (property.additionalProperties) { + if (property.additionalProperties.items?.$ref) { + return property.additionalProperties.items.$ref.name + } + + return property.additionalProperties.$ref?.name || '' + } + + if (property.items) { + return property.items.$ref?.name || '' + } + + return property.$ref?.name || '' +} + +function propertyParsingSnippet(prop: Property): string | null { + const baseP = (prop.items ? prop.items : prop) as Property; + const baseRef = getBaseRef(prop); + + if (helpers.isDateTime(baseP)) { + return `cast(dateFromJson, obj.${prop.name})`; + } else if (isBuffer(baseP)) { + return `cast(bufferFromJson, obj.${prop.name})`; + } else if (helpers.isMap(prop)) { + return mapParsingSnippet(`obj.${prop.name}`, prop); + } else if (!helpers.isPrimitive(baseP)) { + return `cast(${baseRef}.fromJson, obj.${prop.name})`; + } + return null; +} +function mapParsingSnippet(source: string, prop: Property): string { + // Extract map's value type, handling both direct and array cases + const valueType = prop.additionalProperties?.items || prop.additionalProperties; + if (!valueType) return `mapFromJson(${source}, (v) => v)`; + + // Get type annotation for the resulting map values + const typeAnnotation = getTypeAnnotation(valueType as Property); + + // Generate converter based on value type + const valueConverter = buildParsingConverter(valueType as Property); + + // Handle arrays + if (prop.additionalProperties?.items) { + return `mapFromJson(${source}, (arr: any[]): ${typeAnnotation}[] => arr.map((v: any) => ${valueConverter}(v)))`; + } + + // Handle simple maps + return `mapFromJson(${source}, ${valueConverter})`; +} + +function buildParsingConverter(prop: Property): string { + // Handle nested maps + if (helpers.isMap(prop)) { + const innerValueType = prop.additionalProperties as Property; + const valueRef = getBaseRef(innerValueType); + + if (!helpers.isPrimitive(innerValueType)) { + return `(v: any) => mapFromJson(v, ${valueRef}.fromJson)`; + } + return `(v: any) => mapFromJson(v, (vv) => vv as ${toTypeScriptType(innerValueType)})`; + } + + // Handle non-map types + const ref = getBaseRef(prop); + if (!helpers.isPrimitive(prop)) { + return `${ref}.fromJson`; + } + return `(v: any) => v as ${toTypeScriptType(prop)}`; +} + +function getTypeAnnotation(prop: Property): string { + if (helpers.isMap(prop)) { + const valueType = prop.additionalProperties as Property; + return getBaseRef(valueType) || toTypeScriptType(valueType); + } + return getBaseRef(prop) || toTypeScriptType(prop); +} + +function propertyToJsonSnippet(prop: Property): string | null { + const baseP = (prop.items ? prop.items : prop) as Property; + const baseRef = getBaseRef(prop); + + if (helpers.isDateTime(baseP)) { + return `cast(dateToJson, obj.${prop.name})`; + } else if (isBuffer(baseP)) { + return `cast(bufferToJson, obj.${prop.name})`; + } else if (helpers.isMap(prop)) { + return mapToJsonSnippet(`obj.${prop.name}`, prop); + } else if (!helpers.isPrimitive(baseP)) { + return `cast(${baseRef}.toJson, obj.${prop.name})`; + } + return null; +} + +function mapToJsonSnippet(source: string, prop: Property): string { + // Extract map's value type, handling both direct and array cases + const valueType = prop.additionalProperties?.items || prop.additionalProperties; + if (!valueType) return `mapToJson(${source}, (v) => v)`; + + // Generate converter based on value type + const valueConverter = buildValueConverter(valueType as Property); + + // Handle arrays + if (prop.additionalProperties?.items) { + return `mapToJson(${source}, (arr) => arr.map((v) => ${valueConverter}(v)))`; + } + + // Handle simple maps + return `mapToJson(${source}, ${valueConverter})`; +} + +function buildValueConverter(prop: Property): string { + // Handle nested maps + if (helpers.isMap(prop)) { + const innerValueType = prop.additionalProperties as Property; + const valueRef = getBaseRef(innerValueType); + + if (!helpers.isPrimitive(innerValueType)) { + return `(v) => mapToJson(v, ${valueRef}.toJson)`; + } + return `(v) => mapToJson(v, (vv) => vv)`; + } + + // Handle non-map types + const ref = getBaseRef(prop); + if (!helpers.isPrimitive(prop)) { + return `${ref}.toJson`; + } + return `(v) => v`; +} + // TODO: can move this helper up to shared library? function isBuffer(property: Property | Parameter): boolean { return property.type === 'buffer' @@ -61,7 +195,12 @@ export function render() { ...helpers, isBuffer, toTypeScriptType, + getBaseRef, + propertyToJsonSnippet, + mapToJsonSnippet, + propertyParsingSnippet, + mapParsingSnippet } const output = ejs.render(tmpl, ctx) Host.outputString(output) -} +} \ No newline at end of file diff --git a/template/src/index.ts.ejs b/template/src/index.ts.ejs index ee39b55..3c83830 100644 --- a/template/src/index.ts.ejs +++ b/template/src/index.ts.ejs @@ -4,6 +4,9 @@ import { <% Object.values(schema.schemas).forEach(schema => { -%> <%- schema.name %>, <% }) -%> + + mapFromJson, + mapToJson, } from './pdk' <% } %> @@ -14,7 +17,10 @@ export function <%- ex.name %>(): number { <% if (isBuffer(ex.input)) { -%> const input: <%- toTypeScriptType(ex.input) %> = Host.base64ToArrayBuffer(JSON.parse(Host.inputString())) <% } else if (isPrimitive(ex.input)) { -%> - const input: <%- toTypeScriptType(ex.input) %> = JSON.parse(Host.inputString()) + const input: <%- toTypeScriptType(ex.input) %> = JSON.parse(Host.inputString()) + <% } else if (isMap(ex.input)) { -%> + const untypedInput = JSON.parse(Host.inputString()) + const input = <%- mapParsingSnippet('untypedInput', ex.input) %> <% } else { -%> const untypedInput = JSON.parse(Host.inputString()) const input = <%- toTypeScriptType(ex.input) %>.fromJson(untypedInput) @@ -44,6 +50,9 @@ export function <%- ex.name %>(): number { Host.outputString(JSON.stringify(Host.arrayBufferToBase64(output))) <% } else if (isPrimitive(ex.output)) { -%> Host.outputString(JSON.stringify(output)) + <% } else if (isMap(ex.output)) { -%> + const untypedOutput = <%- mapToJsonSnippet('output', ex.output) %> + Host.outputString(JSON.stringify(untypedOutput)) <% } else { -%> const untypedOutput = <%- toTypeScriptType(ex.output) %>.toJson(output) Host.outputString(JSON.stringify(untypedOutput)) diff --git a/template/src/pdk.ts.ejs b/template/src/pdk.ts.ejs index f1f2a17..8d0bb69 100644 --- a/template/src/pdk.ts.ejs +++ b/template/src/pdk.ts.ejs @@ -12,6 +12,30 @@ function cast(caster: (v: any) => any, v: any): any { return caster(v) } +export function mapFromJson(obj: any, cast: (value: any) => T): Record { + const result: Record = {}; + + if (!obj) return result + + for (const [key, value] of Object.entries(obj)) { + result[key] = cast(value); + } + + return result; +} + +export function mapToJson(obj: Record, cast: (value: T) => any): any { + const result: Record = {}; + + if (!obj) return result + + for (const [key, value] of Object.entries(obj)) { + result[key] = cast(value); + } + + return result; +} + function dateToJson(v: Date): string { return v.toISOString() } @@ -48,31 +72,21 @@ export class <%- schema.name %> { return { ...obj, <% schema.properties.forEach(p => { -%> - <% let baseP = p.items ? p.items : p -%> - <% let baseRef = p.$ref ? p.$ref.name : (p.items && p.items.$ref ? p.items.$ref.name : null) -%> - <% if (isDateTime(baseP)) { -%> - <%- p.name -%>: cast(dateFromJson, obj.<%- p.name -%>), - <% } else if (isBuffer(baseP)) {-%> - <%- p.name -%>: cast(bufferFromJson, obj.<%- p.name -%>), - <% } else if (!isPrimitive(baseP)) {-%> - <%- p.name -%>: cast(<%- baseRef -%>.fromJson, obj.<%- p.name -%>), + <% let prop = propertyParsingSnippet(p) -%> + <% if (prop) { -%> + <%- p.name -%>: <%- prop %>, <% } -%> <% }) -%> } } - static toJson(obj: <%- schema.name %>): any{ + static toJson(obj: <%- schema.name %>): any { return { ...obj, <% schema.properties.forEach(p => { -%> - <% let baseP = p.items ? p.items : p -%> - <% let baseRef = p.$ref ? p.$ref.name : (p.items && p.items.$ref ? p.items.$ref.name : null) -%> - <% if (isDateTime(baseP)) { -%> - <%- p.name -%>: cast(dateToJson, obj.<%- p.name -%>), - <% } else if (isBuffer(baseP)) {-%> - <%- p.name -%>: cast(bufferToJson, obj.<%- p.name -%>), - <% } else if (!isPrimitive(baseP)) {-%> - <%- p.name -%>: cast(<%- baseRef -%>.toJson, obj.<%- p.name -%>), + <% let prop = propertyToJsonSnippet(p) -%> + <% if (prop) { -%> + <%- p.name %>: <%- prop %>, <% } -%> <% }) -%> } @@ -117,6 +131,9 @@ export function <%- imp.name %>(<%- imp.input ? `input: ${toTypeScriptType(imp.i <% if (isJsonEncoded(imp.input)) { -%> <% if (isPrimitive(imp.input)) { %> const mem = Memory.fromJsonObject(input as any) + <% } else if (isMap(imp.input)) { %> + const serialized = <%- mapToJsonSnippet('input', imp.input) %> + const mem = Memory.fromJsonObject(serialized) <% } else { %> const casted = <%- toTypeScriptType(imp.input) %>.toJson(input) const mem = Memory.fromJsonObject(casted) @@ -138,6 +155,9 @@ export function <%- imp.name %>(<%- imp.input ? `input: ${toTypeScriptType(imp.i <% if (isJsonEncoded(imp.output)) { -%> <% if (isPrimitive(imp.output)) { -%> return Memory.find(ptr).readJsonObject(); + <% } else if (isMap(imp.output)) { -%> + const output = Memory.find(ptr).readJsonObject(); + return <%- mapParsingSnippet('output', imp.output) %> <% } else { -%> const output = Memory.find(ptr).readJsonObject(); return <%- toTypeScriptType(imp.output) %>.fromJson(output) diff --git a/tests/schemas/fruit.yaml b/tests/schemas/fruit.yaml index 27c17d6..a074a01 100644 --- a/tests/schemas/fruit.yaml +++ b/tests/schemas/fruit.yaml @@ -1,4 +1,23 @@ exports: + mapExport: + description: This demonstrates how you can create an export that takes a map of + strings and returns a map of strings. + input: + type: object + additionalProperties: + type: array + items: + $ref: "#/components/schemas/WriteParams" + description: A map of strings + contentType: application/json + output: + type: object + additionalProperties: + type: array + items: + type: string + description: A map of strings + contentType: application/json topLevelBuffJSON: description: Top level json buffers input: @@ -55,6 +74,23 @@ exports: contentType: application/json $ref: "#/components/schemas/ComplexObject" imports: + mapImport: + input: + type: object + additionalProperties: + type: array + items: + $ref: "#/components/schemas/WriteParams" + description: A map of strings + contentType: application/json + output: + type: object + additionalProperties: + type: array + items: + type: string + description: A map of strings + contentType: application/json eatAFruit: input: contentType: text/plain; charset=utf-8 @@ -78,7 +114,6 @@ imports: description: the raw byte values at key kv_write: description: kvwrite - contentType: application/json input: contentType: application/json "$ref": "#/components/schemas/WriteParams" @@ -138,4 +173,14 @@ components: writeParams: "$ref": "#/components/schemas/WriteParams" nullable: true + anIntMap: + type: object + additionalProperties: + type: integer + format: int32 + description: A map of integers + anObjectMap: + type: object + additionalProperties: + $ref: "#/components/schemas/WriteParams" description: A complex json object