Skip to content

Commit

Permalink
Refactor Bindgen
Browse files Browse the repository at this point in the history
  • Loading branch information
bhelx committed Oct 28, 2024
1 parent d8342a1 commit 68e89cf
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 90 deletions.
25 changes: 21 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"typescript": "^5.3.2"
},
"dependencies": {
"@dylibso/xtp-bindgen": "1.0.0-rc.11",
"@dylibso/xtp-bindgen": "file:///Users/ben/dylibso/xtp/xtp-bindgen",
"ejs": "^3.1.10"
}
}
135 changes: 86 additions & 49 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,65 +1,102 @@
import ejs from 'ejs'
import { helpers, getContext, Property, Parameter } from "@dylibso/xtp-bindgen"
import { helpers, getContext, ObjectType, EnumType, ArrayType, XtpNormalizedType, MapType, Parameter, Property } from "@dylibso/xtp-bindgen"

function toTypeScriptType(property: Property | Parameter): string {
let tp
if (property.$ref) {
tp = property.$ref.name
} else {
switch (property.type) {
case "string":
if (property.format === 'date-time') {
tp = 'Date'
} else {
tp = "string"
}
break
case "integer":
if (property.format === 'int64') {
throw Error(`We do not support format int64 yet`)
} else {
tp = "number"
}
break
case "number":
tp = "number"
break
case "boolean":
tp = "boolean"
break
case "object":
tp = "any"
break
case "array":
if (!property.items) {
tp = 'Array<any>'
} else {
// TODO this is not quite right to force cast
tp = `Array<${toTypeScriptType(property.items as Property)}>`
}
break
case "buffer":
tp = "ArrayBufferLike"
break
}
function toTypeScriptTypeX(type: XtpNormalizedType): string {
// annotate with null if nullable
const nullify = (t: string) => `${t}${type.nullable ? ' | null' : ''}`

switch (type.kind) {
case 'string':
return nullify('string')
case 'int32':
case 'float':
case 'double':
case 'byte':
return nullify('number')
case 'date-time':
return nullify('Date')
case 'boolean':
return nullify('boolean')
case 'array':
const arrayType = type as ArrayType
return nullify(toTypeScriptTypeX(arrayType.elementType))
case 'buffer':
return nullify('ArrayBufferLike')
case 'object':
return nullify((type as ObjectType).name)
case 'enum':
return nullify((type as EnumType).name)
case 'map':
const { keyType, valueType } = type as MapType
return nullify(`Record<${toTypeScriptTypeX(keyType)}, ${toTypeScriptTypeX(valueType)}>`)
case 'int64':
throw Error(`We do not support format int64 yet`)
default:
throw new Error("Cant convert property to typescript type: " + JSON.stringify(type))
}
}

type XtpTyped = { xtpType: XtpNormalizedType }

function toTypeScriptType(property: XtpTyped): string {
return toTypeScriptTypeX(property.xtpType!)
}

/**
* Derives the name of the function responsible for casting the type
*/
function castingFunction(t: XtpNormalizedType, direction: 'From' | 'To'): string {
switch (t.kind) {
case 'object':
return `${(t as ObjectType).name}.${direction.toLowerCase()}Json`
case 'array':
return castingFunction((t as ArrayType).elementType, direction)
case 'map':
return castingFunction((t as MapType).valueType, direction)
case 'date-time':
return `date${direction}Json`
case 'buffer':
return `buffer${direction}Json`
default:
throw new Error(`Type not meant to be casted ${JSON.stringify(t)}`)
}
}

if (!tp) throw new Error("Cant convert property to typescript type: " + property.type)
if (!property.nullable) return tp
return `${tp} | null`
/**
* Check whether this type needs to be cast or not
*/
function isCastable(t: XtpNormalizedType): boolean {
if (['object', 'date-time', 'buffer'].includes(t.kind)) return true

switch (t.kind) {
case 'array':
return isCastable((t as ArrayType).elementType)
case 'map':
return isCastable((t as MapType).valueType)
default:
return false
}
}

// TODO: can move this helper up to shared library?
function isBuffer(property: Property | Parameter): boolean {
return property.type === 'buffer'
/**
* Renders the function call to cast the value
* Assumes the target is called `obj`
*
* Example: cast(dateFromJson, obj.myDateValue)
*/
function castExpression(t: XtpNormalizedType, propName: string, direction: 'From' | 'To'): string {
let cast = 'cast'
if (['array', 'map'].includes(t.kind)) cast = 'castValues'
return `${cast}(${castingFunction(t, direction)}, obj.${propName})`
}

export function render() {
const tmpl = Host.inputString()
const ctx = {
...getContext(),
...helpers,
isBuffer,
isCastable,
castExpression,
toTypeScriptType,
}
const output = ejs.render(tmpl, ctx)
Expand Down
12 changes: 6 additions & 6 deletions template/src/index.ts.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ export function <%- ex.name %>(): number {
<% if (isJsonEncoded(ex.input)) { -%>
<% 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())
<% } else { -%>
<% } else if (isObject(ex.input)) { -%>
const untypedInput = JSON.parse(Host.inputString())
const input = <%- toTypeScriptType(ex.input) %>.fromJson(untypedInput)
<% } else { -%>
const input: <%- toTypeScriptType(ex.input) %> = JSON.parse(Host.inputString())
<% } -%>
<% } else if (ex.input.type === 'string') { -%>
const input = Host.inputString() <%- (ex.input.$ref && ex.input.$ref.enum) ? `as ${ex.input.$ref.name}` : "" %>
Expand All @@ -42,11 +42,11 @@ export function <%- ex.name %>(): number {
<% if (isJsonEncoded(ex.output)) { -%>
<% if (isBuffer(ex.output)) { -%>
Host.outputString(JSON.stringify(Host.arrayBufferToBase64(output)))
<% } else if (isPrimitive(ex.output)) { -%>
Host.outputString(JSON.stringify(output))
<% } else { -%>
<% } else if (isObject(ex.output)) { -%>
const untypedOutput = <%- toTypeScriptType(ex.output) %>.toJson(output)
Host.outputString(JSON.stringify(untypedOutput))
<% } else { -%>
Host.outputString(JSON.stringify(output))
<% } -%>
<% } else if (ex.output.type === 'string') { -%>
Host.outputString(output)
Expand Down
62 changes: 32 additions & 30 deletions template/src/pdk.ts.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,24 @@ function isNull(v: any): boolean {

function cast(caster: (v: any) => any, v: any): any {
if (isNull(v)) return v
if (Array.isArray(v)) return v.map(caster)
return caster(v)
}

function castValues(caster: (v: any) => any, v: any): any {
if (isNull(v)) return v
caster = cast.bind(null, caster) // bind to null preserving logic in `cast`

// if it's an array just map it
if (Array.isArray(v)) return v.map(caster)

// if it's not an array let's assume it's a map
const newMap: any = {}
for (const k in v) {
newMap[k] = caster(v[k])
}
return newMap
}

function dateToJson(v: Date): string {
return v.toISOString()
}
Expand Down Expand Up @@ -40,22 +54,16 @@ export class <%- schema.name %> {
* <%- formatCommentBlock(p.description) %>
*/
<% } -%>
<%- (!p.required || toTypeScriptType(p) === 'any') ? null : '// @ts-expect-error TS2564\n' -%>
<%- p.name %><%- !p.required ? '?' : null %>: <%- toTypeScriptType(p) %>;
<%- (!p.xtpType.required || toTypeScriptType(p) === 'any') ? null : '// @ts-expect-error TS2564\n' -%>
<%- p.name %><%- !p.xtpType.required ? '?' : null %>: <%- toTypeScriptType(p) %>;
<% }) %>
static fromJson(obj: any): <%- 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 -%>),
<% if (isCastable(p.xtpType)) { -%>
<%- p.name -%>: <%- castExpression(p.xtpType, p.name, 'From') %>,
<% } -%>
<% }) -%>
}
Expand All @@ -65,20 +73,14 @@ 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(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 -%>),
<% if (isCastable(p.xtpType)) { -%>
<%- p.name -%>: <%- castExpression(p.xtpType, p.name, 'To') %>,
<% } -%>
<% }) -%>
}
}
}
<% } else if (schema.enum) { %>
<% } else if (isEnum(schema)) { %>
/**
* <%- formatCommentLine(schema.description) %>
Expand Down Expand Up @@ -115,15 +117,15 @@ export enum <%- schema.name %> {
export function <%- imp.name %>(<%- imp.input ? `input: ${toTypeScriptType(imp.input)}` : null %>) <%- imp.output ? `:${toTypeScriptType(imp.output)}` : null %> {
<% if (imp.input) { -%>
<% if (isJsonEncoded(imp.input)) { -%>
<% if (isPrimitive(imp.input)) { %>
const mem = Memory.fromJsonObject(input as any)
<% } else { %>
const casted = <%- toTypeScriptType(imp.input) %>.toJson(input)
<% if (isObject(imp.input)) { %>
const casted = <%- castingFunction(imp.input.xtpType, 'From') %>(input)
const mem = Memory.fromJsonObject(casted)
<% } else { %>
const mem = Memory.fromJsonObject(input as any)
<% } %>
<% } else if (isUtf8Encoded(imp.input)) { -%>
const mem = Memory.fromString(input as string)
<% } else if (imp.input.type === 'string') { -%>
<% } else if (isString(imp.input.type)) { -%>
const mem = Memory.fromString(input)
<% } else { -%>
const mem = Memory.fromBuffer(input)
Expand All @@ -136,15 +138,15 @@ export function <%- imp.name %>(<%- imp.input ? `input: ${toTypeScriptType(imp.i
<% if (imp.output) { -%>
<% if (isJsonEncoded(imp.output)) { -%>
<% if (isPrimitive(imp.output)) { -%>
return Memory.find(ptr).readJsonObject();
<% } else { -%>
<% if (isObject(imp.output)) { -%>
const output = Memory.find(ptr).readJsonObject();
return <%- toTypeScriptType(imp.output) %>.fromJson(output)
return <%- castingFunction(imp.output.xtpType, 'To') %>(output)
<% } else { -%>
return Memory.find(ptr).readJsonObject();
<% } -%>
<% } else if (isUtf8Encoded(imp.output)) { -%>
return Memory.find(ptr).readString();
<% } else if (imp.output.type === 'string') { -%>
<% } else if (isString(imp.output)) { -%>
return Memory.find(ptr).readString();
<% } else { -%>
return Memory.find(ptr).readBytes();
Expand Down
24 changes: 24 additions & 0 deletions tests/schemas/fruit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,14 @@ components:
items:
type: string
format: date-time
arrayOfEnum:
type: array
items:
"$ref": "#/components/schemas/GhostGang"
arrayOfObjects:
type: array
items:
"$ref": "#/components/schemas/WriteParams"
ghost:
"$ref": "#/components/schemas/GhostGang"
description: I can override the description for the property here
Expand All @@ -138,4 +146,20 @@ components:
writeParams:
"$ref": "#/components/schemas/WriteParams"
nullable: true
aStringMap:
type: string
additionalProperties:
type: string
aWriteParamMap:
type: string
additionalProperties:
"$ref": "#/components/schemas/WriteParams"
aNullableWriteParamMap:
type: string
nullable: true
additionalProperties:
"$ref": "#/components/schemas/WriteParams"
aBuffer:
type: buffer
nullable: true
description: A complex json object

0 comments on commit 68e89cf

Please sign in to comment.