diff --git a/package-lock.json b/package-lock.json index ddfad19..532d844 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.1", "license": "BSD-3-Clause", "dependencies": { - "@dylibso/xtp-bindgen": "1.0.0-rc.8", + "@dylibso/xtp-bindgen": "1.0.0-rc.13", "ejs": "^3.1.10" }, "devDependencies": { @@ -21,9 +21,9 @@ } }, "node_modules/@dylibso/xtp-bindgen": { - "version": "1.0.0-rc.8", - "resolved": "https://registry.npmjs.org/@dylibso/xtp-bindgen/-/xtp-bindgen-1.0.0-rc.8.tgz", - "integrity": "sha512-9PVXiNa9xL+LNdn0wTY7cUzYiB44RIO/HNIXBeZZSyaA2Tc0rJl97TPcNPpJvQhNUwQVfjWDxCRXYULpUKf/nw==" + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@dylibso/xtp-bindgen/-/xtp-bindgen-1.0.0-rc.13.tgz", + "integrity": "sha512-aCmYSgC3Xwdlhm8mBKVpkzuHAtQ84ZgIwkdYQ3QFmDRxnBlZUtOZgpRoJTtOnahm0lUCiBkWkLcsikUluMY/qw==" }, "node_modules/@esbuild/aix-ppc64": { "version": "0.19.12", diff --git a/package.json b/package.json index 455db46..1a328ad 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "typescript": "^5.3.2" }, "dependencies": { - "@dylibso/xtp-bindgen": "1.0.0-rc.8", + "@dylibso/xtp-bindgen": "1.0.0-rc.13", "ejs": "^3.1.10" } } diff --git a/src/index.ts b/src/index.ts index 177fab2..fbe27ac 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,73 +1,71 @@ import ejs from "ejs"; -import { getContext, helpers, Property } from "@dylibso/xtp-bindgen"; +import { ArrayType, EnumType, getContext, helpers, MapType, ObjectType, Property, XtpNormalizedType, XtpTyped } from "@dylibso/xtp-bindgen"; -function toRustType(property: Property): string { - if (property.$ref) return `types::${helpers.capitalize(property.$ref.name)}`; - switch (property.type) { - case "string": - if (property.format === "date-time") { - return "chrono::DateTime"; - } - return "String"; - case "number": - if (property.format === "float") { - return "f32"; - } - if (property.format === "double") { - return "f64"; - } - return "i64"; - case "integer": - return "i32"; - case "boolean": - return "bool"; - case "object": - return "std::collections::HashMap"; - case "array": - if (!property.items) return "Vec"; - return `Vec<${toRustType(property.items as Property)}>`; - case "buffer": - return "Vec"; - default: - throw new Error("Can't convert property to Rust type: " + property.type); +function toRustTypeX(type: XtpNormalizedType): string { + // turn into reference pointer if needed + const optionalize = (t: string) => { + return type.nullable ? `Option<${t}>` : t } -} -function jsonWrappedRustType(property: Property): string { - if (property.$ref) return `Json`; - switch (property.type) { - case "string": - if (property.format === "date-time") { - return "Json>"; - } - return "String"; - case "number": - if (property.format === "float") { - return "Json"; - } - if (property.format === "double") { - return "Json"; + switch (type.kind) { + case 'string': + return optionalize('String') + case 'int32': + return optionalize('i32') + case 'float': + return optionalize('f32') + case 'double': + return optionalize('f64') + case 'byte': + return optionalize('byte') + case 'date-time': + return optionalize("chrono::DateTime") + case 'boolean': + return optionalize('bool') + case 'array': + const arrayType = type as ArrayType + return optionalize(`Vec<${toRustTypeX(arrayType.elementType)}>`) + case 'buffer': + return optionalize('Vec') + case 'object': + const oType = (type as ObjectType) + if (oType.properties?.length > 0) { + return optionalize(`types::${helpers.capitalize(oType.name)}`) + } else { + // we're just exposing the serde values directly for backwards compat + return optionalize("std::collections::HashMap") } - return "Json"; - case "integer": - return "Json"; - case "boolean": - return "Json"; - case "object": - return "Json>"; - case "array": - if (!property.items) return "Json>"; - // TODO this is not quite right to force cast - return `Json>`; - case "buffer": - return "Vec"; + case 'enum': + return optionalize(`types::${helpers.capitalize((type as EnumType).name)}`) + case 'map': + const { keyType, valueType } = type as MapType + return optionalize(`std::collections::HashMap<${toRustTypeX(keyType)}, ${toRustTypeX(valueType)}>`) default: - throw new Error("Can't convert property to Rust type: " + property.type); + throw new Error("Can't convert XTP type to Rust type: " + type) } } -function makePublic(s: string) { - return "pub " + s; +function isOptional(type: String): boolean { + return type.startsWith('Option<') +} + +function toRustType(property: XtpTyped, required?: boolean): string { + const t = toRustTypeX(property.xtpType) + + // if required is unset, just return what we get back + if (required === undefined) return t + + // if it's set and true, just return what we get back + if (required) return t + + // otherwise it's false, assuming it's not already, + // wrap it in an Option + if (t.startsWith('Option<')) return t + return `Option<${t}>` +} + +function jsonWrappedRustType(property: Property): string { + return `Json<${toRustType(property)}>` } export function render() { @@ -76,7 +74,7 @@ export function render() { ...helpers, ...getContext(), toRustType, - makePublic, + isOptional, jsonWrappedRustType, }; diff --git a/template/src/pdk.rs.ejs b/template/src/pdk.rs.ejs index 1c5fc45..8c343bd 100644 --- a/template/src/pdk.rs.ejs +++ b/template/src/pdk.rs.ejs @@ -4,6 +4,10 @@ #![allow(unused_macros)] use extism_pdk::*; +fn panic_if_key_missing() -> ! { + panic!("missing key"); +} + pub(crate) mod internal { pub(crate) fn return_error(e: extism_pdk::Error) -> i32 { let err = format!("{:?}", e); @@ -48,10 +52,10 @@ mod exports { #[no_mangle] pub extern "C" fn <%- ex.name %>() -> i32 { - <% if (ex.output && ex.output.contentType === "application/json") { %> - let ret = crate::<%- camelToSnakeCase(ex.name) %>(<% if (ex.input) { %> <% if (ex.input.contentType === "application/json") { %> try_input_json!() <% } else { %> try_input!() <% } %> <% } %>).and_then(|x| extism_pdk::output(extism_pdk::Json(x))); + <% if (ex.output && isJsonEncoded(ex.output)) { %> + let ret = crate::<%- camelToSnakeCase(ex.name) %>(<% if (ex.input) { %> <% if (isJsonEncoded(ex.input)) { %> try_input_json!() <% } else { %> try_input!() <% } %> <% } %>).and_then(|x| extism_pdk::output(extism_pdk::Json(x))); <% } else { %> - let ret = crate::<%- camelToSnakeCase(ex.name) %>(<% if (ex.input) { %> <% if (ex.input.contentType === "application/json") { %> try_input_json!() <% } else { %> try_input!() <% } %> <% } %>).and_then(extism_pdk::output); + let ret = crate::<%- camelToSnakeCase(ex.name) %>(<% if (ex.input) { %> <% if (isJsonEncoded(ex.input)) { %> try_input_json!() <% } else { %> try_input!() <% } %> <% } %>).and_then(extism_pdk::output); <% } %> match ret { Ok(()) => { @@ -68,33 +72,37 @@ pub extern "C" fn <%- ex.name %>() -> i32 { pub mod types { use super::*; <% Object.values(schema.schemas).forEach(schema => { %> - <% if (schema.enum) { %> -#[derive(serde::Serialize, serde::Deserialize, extism_pdk::FromBytes, extism_pdk::ToBytes)] + <% if (isEnum(schema)) { %> +#[derive(Default, serde::Serialize, serde::Deserialize, extism_pdk::FromBytes, extism_pdk::ToBytes)] #[encoding(Json)] pub enum <%- capitalize(schema.name) %> { - <% schema.enum.forEach(variant => { -%> + #[default] + <% schema.xtpType.values.forEach(variant => { -%> #[serde(rename = "<%- variant %>")] <%- capitalize(variant) %>, <% }) %> } <% } else { %> -#[derive(serde::Serialize, serde::Deserialize, extism_pdk::FromBytes, extism_pdk::ToBytes)] +#[derive(Default, serde::Serialize, serde::Deserialize, extism_pdk::FromBytes, extism_pdk::ToBytes)] #[encoding(Json)] pub struct <%- capitalize(schema.name) %> { <% schema.properties.forEach(p => { -%> + <% let propType = toRustType(p, p.required) %> <% if (p.description) { -%> /// <%- formatCommentBlock(p.description, "/// ") %> <% } -%> #[serde(rename = "<%- p.name %>")] - <% if (p.nullable) { %>#[serde(default)]<% } %> - <% if (p.type === "buffer") { %> #[serde(with = "Base64Standard")] <% } %> - <%- makePublic(camelToSnakeCase(p.name)) %>: <%- p.nullable ? `Option<${toRustType(p)}>` : toRustType(p) %>, - <% }) %> - - <% if (schema.additionalProperties) { %> - #[serde(flatten)] - additional_properties: std::collections::HashMap>, + <% if (isOptional(propType)) { %> + <% if (!p.required) { %> + #[serde(skip_serializing_if="Option::is_none")] + #[serde(default = "panic_if_key_missing")] + <% } else { %> + #[serde(default)] + <% } %> <% } %> + <% if (isBuffer(p)) { %> #[serde(with = "Base64Standard")] <% } %> + pub <%- camelToSnakeCase(p.name) %>: <%- propType %>, + <% }) %> } <% } %> <% }); %> @@ -105,7 +113,7 @@ mod raw_imports { #[host_fn] extern "ExtismHost" { <% schema.imports.forEach(imp => { %> - pub(crate) fn <%- imp.name %>(<% if (imp.input) { -%>input: <% if (imp.input.contentType === "application/json") { %><%- jsonWrappedRustType(imp.input) %><%} else {%> <%- toRustType(imp.input) %> <%}%><%} -%>) <% if (imp.output) { -%> -> <% if (imp.output.contentType === "application/json") {%> <%- jsonWrappedRustType(imp.output) %> <%} else {%> <%-toRustType(imp.output) %> <%}%><% } -%>; + pub(crate) fn <%- imp.name %>(<% if (imp.input) { -%>input: <% if (isJsonEncoded(imp.input)) { %><%- jsonWrappedRustType(imp.input) %><%} else {%> <%- toRustType(imp.input) %> <%}%><%} -%>) <% if (imp.output) { -%> -> <% if (isJsonEncoded(imp.output)) {%> <%- jsonWrappedRustType(imp.output) %> <%} else {%> <%-toRustType(imp.output) %> <%}%><% } -%>; <% }) %> } } @@ -123,11 +131,11 @@ mod raw_imports { pub(crate) fn <%- camelToSnakeCase(imp.name) %>(<% if (imp.input) { -%>input: <%- toRustType(imp.input) %><%} -%>) -> std::result::Result<<% if (imp.output) { -%><%- toRustType(imp.output) %><% } else { -%> () <% } %> , extism_pdk::Error> { let res = unsafe { raw_imports::<%- imp.name %>( - <% if (imp.input) { %> <% if (imp.input.contentType === "application/json") { %> extism_pdk::Json(input) <%} else {%> input <%}%> <% } %> + <% if (imp.input) { %> <% if (isJsonEncoded(imp.input)) { %> extism_pdk::Json(input) <%} else {%> input <%}%> <% } %> )? }; - <% if (imp.output && imp.output.contentType === "application/json") { %> + <% if (imp.output && isJsonEncoded(imp.output)) { %> let extism_pdk::Json(res) = res; <% } %> diff --git a/tests/schemas/fruit.yaml b/tests/schemas/fruit.yaml index bdf0d1f..bb70826 100644 --- a/tests/schemas/fruit.yaml +++ b/tests/schemas/fruit.yaml @@ -118,3 +118,23 @@ components: "$ref": "#/components/schemas/WriteParams" nullable: true description: A complex json object + RequiredVsNullable: + required: + - aRequiredNotNullableBoolean + - aRequiredNullableBoolean + properties: + aRequiredNotNullableBoolean: + type: boolean + description: A not-nullable boolean prop + aRequiredNullableBoolean: + type: boolean + description: A nullable boolean prop + nullable: true + aNotRequiredNotNullableBoolean: + type: boolean + description: A not-nullable boolean prop + aNotRequiredNullableBoolean: + type: boolean + description: A nullable boolean prop + nullable: true + description: A weird little object diff --git a/tests/schemas/random.yaml b/tests/schemas/random.yaml new file mode 100644 index 0000000..6a1d71a --- /dev/null +++ b/tests/schemas/random.yaml @@ -0,0 +1,422 @@ +--- +version: v1-draft +exports: + noInputExport: + description: Export with no input + output: + type: string + contentType: text/plain; charset=utf-8 + description: Simple output + noOutputExport: + description: Export with no output + input: + type: string + contentType: text/plain; charset=utf-8 + description: Simple input + emptyExport: + description: Export with neither input nor output + stringToStringExport: + description: Export with string input and output + input: + type: string + contentType: text/plain; charset=utf-8 + description: Plain text input + output: + type: string + contentType: text/plain; charset=utf-8 + description: Plain text output + stringToJsonExport: + description: Export with string input and JSON output + input: + type: string + contentType: text/plain; charset=utf-8 + description: Plain text input + output: + type: object + contentType: application/json + description: JSON output + jsonToStringExport: + description: Export with JSON input and string output + input: + type: object + contentType: application/json + description: JSON input + output: + type: string + contentType: text/plain; charset=utf-8 + description: Plain text output + bufferToBufferExport: + description: Export with buffer input and output + input: + type: buffer + contentType: application/x-binary + description: Binary input + output: + type: buffer + contentType: application/x-binary + description: Binary output + bufferToJsonExport: + description: Export with buffer input and JSON output + input: + type: buffer + contentType: application/x-binary + description: Binary input + output: + type: object + contentType: application/json + description: JSON output with processing results + jsonToBufferExport: + description: Export with JSON input and buffer output + input: + type: object + contentType: application/json + description: JSON input with configuration + output: + type: buffer + contentType: application/x-binary + description: Generated binary output + simpleJsonExport: + description: Export with simple JSON types + input: + type: object + contentType: application/json + description: Simple JSON input + output: + type: object + contentType: application/json + description: Simple JSON output + complexJsonExport: + description: Export with complex nested JSON + input: + "$ref": "#/components/schemas/ComplexObject" + contentType: application/json + description: Complex JSON input + output: + "$ref": "#/components/schemas/ComplexObject" + contentType: application/json + description: Complex JSON output + arrayJsonExport: + description: Export with JSON arrays + input: + type: array + contentType: application/json + description: Array input + items: + type: object + output: + type: array + contentType: application/json + description: Array output + items: + "$ref": "#/components/schemas/NestedObject" + simpleRefExport: + description: Export with simple schema reference + input: + "$ref": "#/components/schemas/NestedObject" + contentType: application/json + description: Simple reference input + output: + "$ref": "#/components/schemas/NestedObject" + contentType: application/json + description: Simple reference output + arrayRefExport: + description: Export with array of references + input: + type: array + contentType: application/json + description: Array of referenced objects + items: + "$ref": "#/components/schemas/NestedObject" + output: + type: array + contentType: application/json + description: Processed array of objects + items: + "$ref": "#/components/schemas/ComplexObject" + nestedRefExport: + description: Export with nested references + input: + "$ref": "#/components/schemas/ComplexObject" + contentType: application/json + description: Nested reference input + output: + contentType: application/json + description: Nested output structure + "$ref": "#/components/schemas/NestedObject" + mixedTypesExport: + description: Export demonstrating mixed content types and formats + input: + type: object + contentType: application/json + description: Mixed type input object + output: + type: object + contentType: application/json + description: Mixed type response +imports: + noInputImport: + description: Import with no input + output: + type: string + contentType: text/plain; charset=utf-8 + description: Simple output + noOutputImport: + description: Import with no output + input: + type: string + contentType: text/plain; charset=utf-8 + description: Simple input + emptyImport: + description: Import with neither input nor output + stringToStringImport: + description: Import with string input and output + input: + type: string + contentType: text/plain; charset=utf-8 + description: Plain text input + output: + type: string + contentType: text/plain; charset=utf-8 + description: Plain text output + stringToJsonImport: + description: Import with string input and JSON output + input: + type: string + contentType: text/plain; charset=utf-8 + description: Plain text input + output: + type: object + contentType: application/json + description: JSON output + jsonToStringImport: + description: Import with JSON input and string output + input: + type: object + contentType: application/json + description: JSON input + output: + type: string + contentType: text/plain; charset=utf-8 + description: Plain text output + bufferToBufferImport: + description: Import with buffer input and output + input: + type: buffer + contentType: application/x-binary + description: Binary input + output: + type: buffer + contentType: application/x-binary + description: Binary output + bufferToJsonImport: + description: Import with buffer input and JSON output + input: + type: buffer + contentType: application/x-binary + description: Binary input + output: + type: object + contentType: application/json + description: JSON output with processing results + jsonToBufferImport: + description: Import with JSON input and buffer output + input: + type: object + contentType: application/json + description: JSON input with configuration + output: + type: buffer + contentType: application/x-binary + description: Generated binary output + simpleJsonImport: + description: Import with simple JSON types + input: + type: object + contentType: application/json + description: Simple JSON input + output: + type: object + contentType: application/json + description: Simple JSON output + complexJsonImport: + description: Import with complex nested JSON + input: + "$ref": "#/components/schemas/ComplexObject" + contentType: application/json + description: Complex JSON input + output: + "$ref": "#/components/schemas/ComplexObject" + contentType: application/json + description: Complex JSON output + arrayJsonImport: + description: Import with JSON arrays + input: + type: array + contentType: application/json + description: Array input + items: + type: object + output: + type: array + contentType: application/json + description: Array output + items: + "$ref": "#/components/schemas/NestedObject" + simpleRefImport: + description: Import with simple schema reference + input: + "$ref": "#/components/schemas/NestedObject" + contentType: application/json + description: Simple reference input + output: + "$ref": "#/components/schemas/NestedObject" + contentType: application/json + description: Simple reference output + arrayRefImport: + description: Import with array of references + input: + type: array + contentType: application/json + description: Array of referenced objects + items: + "$ref": "#/components/schemas/NestedObject" + output: + type: array + contentType: application/json + description: Processed array of objects + items: + "$ref": "#/components/schemas/ComplexObject" + nestedRefImport: + description: Import with nested references + input: + "$ref": "#/components/schemas/ComplexObject" + contentType: application/json + description: Nested reference input + output: + type: object + contentType: application/json + description: Nested output structure + "$ref": "#/components/schemas/NestedObject" + mixedTypesImport: + description: Import demonstrating mixed content types and formats + input: + type: object + contentType: application/json + description: Mixed type input object + output: + type: object + contentType: application/json + description: Mixed type response + reflectJsonObjectHost: + description: | + This function takes a ComplexObject and returns a ComplexObject. + It should come out the same way it came in. It's the same as the export. + But the export should call this. + input: + contentType: application/json + $ref: "#/components/schemas/ComplexObject" + output: + contentType: application/json + $ref: "#/components/schemas/ComplexObject" + reflectUtf8StringHost: + description: | + This function takes a string and returns it. + Should come out the same way it came in. Same as export. + input: + type: string + description: The input string + contentType: text/plain; charset=utf-8 + output: + type: string + description: The output string + contentType: text/plain; charset=utf-8 + reflectByteBufferHost: + description: | + This function takes a bugger and returns it. + Should come out the same way it came in. Same as export. + input: + contentType: application/x-binary + type: buffer + description: The input byte buffer + output: + contentType: application/x-binary + type: buffer + description: The output byte buffer + + noInputWithOutputHost: + description: a function that takes no input, but returns an output + output: + contentType: text/plain; charset=utf-8 + type: string + + withInputNoOutputHost: + description: a function that takes input, but returns no output + input: + contentType: application/json + type: integer + + noInputNoOutputHost: + description: a function that takes no input, and returns no output +components: + schemas: + ComplexObject: + description: Object with all possible property types + properties: + stringField: + type: string + description: String field + numberField: + type: number + format: double + description: Number field + integerField: + type: integer + format: int64 + description: Integer field + booleanField: + type: boolean + description: Boolean field + arrayField: + type: array + items: + type: string + description: Array field + objectField: + type: object + description: Untyped Object field + nullableField: + type: string + nullable: true + description: Nullable field + referenceField: + "$ref": "#/components/schemas/NestedObject" + description: Reference field + enumField: + "$ref": "#/components/schemas/EnumExample" + description: Enum field + required: + - stringField + - numberField + NestedObject: + description: Object for nested references + properties: + id: + type: string + description: Identifier + value: + type: number + format: double + description: Value + metadata: + type: object + description: Untyped Metadata information + required: + - id + EnumExample: + type: string + description: Example string enum + enum: + - PENDING + - PROCESSING + - COMPLETED + - FAILED diff --git a/tests/test.sh b/tests/test.sh index 6ab79e1..a490a05 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -9,7 +9,7 @@ cd tests for file in ./schemas/*.yaml; do echo "Generating and testing $file..." rm -rf output - xtp plugin init --schema-file $file --template ../bundle --path output -y --name output --feature stub-with-code-samples + xtp plugin init --schema-file "$file" --template ../bundle --path output -y --name output --feature stub-with-code-samples cd output xtp plugin build cd ..