Skip to content
Merged
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"
}
}
142 changes: 80 additions & 62 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,82 +1,100 @@
import ejs from "ejs";
import { getContext, helpers, Property } from "@dylibso/xtp-bindgen";
import ejs from 'ejs';
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";
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);
// keywords via https://doc.rust-lang.org/reference/keywords.html
let KEYWORDS: Set<string> = new Set(['as', 'break', 'const', 'continue', 'crate', 'else', 'enum', 'extern', 'false', 'fn', 'for', 'if', 'impl', 'in', 'let', 'loop', 'match', 'mod', 'move', 'mut', 'pub', 'ref', 'return', 'self', 'Self', 'static', 'struct', 'super', 'trait', 'true', 'type', 'unsafe', 'use', 'where', 'while', 'async', 'await', 'dyn', 'abstract', 'become', 'box', 'do', 'final', 'macro', 'override', 'priv', 'typeof', 'unsized', 'virtual', 'yield', 'try', 'macro_rules', 'union', 'dyn'])

function formatExternIdentifier(ident: string) {
if (KEYWORDS.has(ident)) {
return `r#${ident}`
}
return ident
}

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>";
function formatIdentifier(ident: string): string {
if (KEYWORDS.has(ident)) {
return `r#${ident}`
}

return helpers.camelToSnakeCase(ident)
}

function toRustTypeX(type: XtpNormalizedType): string {
// turn into reference pointer if needed
const optionalize = (t: string) => {
return type.nullable ? `Option<${t}>` : t
}

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('serde_json::Map<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(`serde_json::Map<${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() {
const tmpl = Host.inputString();
const ctx = {
...helpers,
...getContext(),
formatIdentifier,
formatExternIdentifier,
toRustType,
makePublic,
isOptional,
jsonWrappedRustType,
};

Expand Down
4 changes: 2 additions & 2 deletions template/src/lib.rs.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ use extism_pdk::*;
<% if (hasComment(ex)) { -%>
// <%- formatCommentBlock(ex.description, "// ") %>
<% } -%>
pub(crate) fn <%= camelToSnakeCase(ex.name) %>(<%if (ex.input) { %>_input: <%- toRustType(ex.input) %> <% } %>) -> Result<<%if (ex.output) { %> <%- toRustType(ex.output) %> <% } else { %> () <% } %>, Error> {
pub(crate) fn <%= formatIdentifier(ex.name) %>(<%if (ex.input) { %>_input: <%- toRustType(ex.input) %> <% } %>) -> Result<<%if (ex.output) { %> <%- toRustType(ex.output) %> <% } else { %> () <% } %>, Error> {
<% if (featureFlags['stub-with-code-samples'] && codeSamples(ex, 'rust').length > 0) { -%>
<%- codeSamples(ex, 'rust')[0].source %>
<% } else { -%>
todo!("Implement <%= camelToSnakeCase(ex.name) %>")
todo!("Implement <%= formatIdentifier(ex.name) %>")
<% } -%>
}

Expand Down
49 changes: 29 additions & 20 deletions template/src/pdk.rs.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
#![allow(unused_macros)]
use extism_pdk::*;

#[allow(unused)]
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 @@ -47,11 +52,11 @@ mod exports {
<% schema.exports.forEach(ex => { -%>

#[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)));
pub extern "C" fn <%- formatExternIdentifier(ex.name) %>() -> i32 {
<% if (ex.output && isJsonEncoded(ex.output)) { %>
let ret = crate::<%- formatIdentifier(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::<%- formatIdentifier(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 +73,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)]
<% } else { %>
#[serde(default = "panic_if_key_missing")]
<% } %>
<% } %>
<% if (isBuffer(p)) { %> #[serde(with = "Base64Standard")] <% } %>
pub <%- formatIdentifier(p.name) %>: <%- propType %>,
<% }) %>
}
<% } %>
<% }); %>
Expand All @@ -105,7 +114,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 <%- formatExternIdentifier(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 @@ -120,14 +129,14 @@ mod raw_imports {
/// And it returns an output <%- toRustType(imp.output) %> (<%- formatCommentLine(imp.output.description) %>)
<% } -%>
#[allow(unused)]
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> {
pub(crate) fn <%- formatIdentifier(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
53 changes: 53 additions & 0 deletions tests/schemas/fruit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@ exports:
output:
contentType: application/json
$ref: "#/components/schemas/ComplexObject"
fn:
description: |
Try to land on a keyword. Just try it.
codeSamples:
- lang: typescript
source: |
return { ghost: GhostGang.inky, aBoolean: true, aString: "okay", anInt: 123 }
input:
contentType: application/json
$ref: "#/components/schemas/impl"
output:
contentType: application/json
$ref: "#/components/schemas/ComplexObject"
imports:
eatAFruit:
input:
Expand Down Expand Up @@ -69,6 +82,15 @@ imports:
version: v1-draft
components:
schemas:
impl:
properties:
struct:
type: string
description: a keyword
await:
type: string
description: another keyword
description: Really try to land on a keyword please
WriteParams:
properties:
key:
Expand All @@ -84,6 +106,8 @@ components:
- orange
- banana
- strawberry
- mod
- use
description: A set of available fruits you can consume
GhostGang:
enum:
Expand All @@ -97,6 +121,15 @@ components:
ghost:
"$ref": "#/components/schemas/GhostGang"
description: I can override the description for the property here
mod:
type: boolean
description: A boolean prop
use:
type: boolean
description: A boolean prop
await:
type: boolean
description: A boolean prop
aBoolean:
type: boolean
description: A boolean prop
Expand All @@ -118,3 +151,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