Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 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.8",
"@dylibso/xtp-bindgen": "1.0.0-rc.13",
"ejs": "^3.1.10"
}
}
122 changes: 60 additions & 62 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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<chrono::Utc>";
}
return "String";
case "number":
if (property.format === "float") {
return "f32";
}
if (property.format === "double") {
return "f64";
}
return "i64";
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: we have a type for i64 but don't yet support it because of json limitations. i have an issue filed for this.

case "integer":
return "i32";
case "boolean":
return "bool";
case "object":
return "std::collections::HashMap<String, serde_json::Value>";
case "array":
if (!property.items) return "Vec<serde_json::Value>";
return `Vec<${toRustType(property.items as Property)}>`;
case "buffer":
return "Vec<u8>";
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<types::${helpers.capitalize(property.$ref.name)}>`;
switch (property.type) {
case "string":
if (property.format === "date-time") {
return "Json<chrono::DateTime<chrono::Utc>>";
}
return "String";
case "number":
if (property.format === "float") {
return "Json<f32>";
}
if (property.format === "double") {
return "Json<f64>";
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<chrono::Utc>")
case 'boolean':
return optionalize('bool')
case 'array':
const arrayType = type as ArrayType
return optionalize(`Vec<${toRustTypeX(arrayType.elementType)}>`)
case 'buffer':
return optionalize('Vec<u8>')
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<String, serde_json::Value>")
}
return "Json<i64>";
case "integer":
return "Json<i32>";
case "boolean":
return "Json<bool>";
case "object":
return "Json<std::collections::HashMap<String, serde_json::Value>>";
case "array":
if (!property.items) return "Json<Vec<serde_json::Value>>";
// TODO this is not quite right to force cast
return `Json<Vec<${toRustType(property.items as Property)}>>`;
case "buffer":
return "Vec<u8>";
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)}>`)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to use serde_json::Value::Map instead of HashMap while we're here. (serde_json::Map gives the plugin author the ability to preserve key order.)

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() {
Expand All @@ -76,7 +74,7 @@ export function render() {
...helpers,
...getContext(),
toRustType,
makePublic,
isOptional,
jsonWrappedRustType,
};

Expand Down
44 changes: 26 additions & 18 deletions template/src/pdk.rs.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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(()) => {
Expand All @@ -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<String, <%- schema.additionalProperties.type ? toRustType(schema.additionalProperties.type) : "serde_json::Value" %>>,
<% 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 %>,
<% }) %>
}
<% } %>
<% }); %>
Expand All @@ -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) %> <%}%><% } -%>;
<% }) %>
}
}
Expand All @@ -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;
<% } %>

Expand Down
20 changes: 20 additions & 0 deletions tests/schemas/fruit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading
Loading