diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d6dec0e..a53eab3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -63,6 +63,7 @@ jobs: - name: Run Bindgen Test run: | + sh bundle.sh sh bindgen-test.sh install sh bindgen-test.sh run diff --git a/package-lock.json b/package-lock.json index 8ba5d1d..2c6b4d4 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.11", + "@dylibso/xtp-bindgen": "1.0.0-rc.13", "ejs": "^3.1.10" }, "devDependencies": { @@ -21,9 +21,10 @@ } }, "node_modules/@dylibso/xtp-bindgen": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@dylibso/xtp-bindgen/-/xtp-bindgen-1.0.0-rc.11.tgz", - "integrity": "sha512-zXesPfNHKaEK3IwMKFW5qk4UoJTazxmslpyUYn6n4FffZvY7QPBOSomyNVaNRtTr3ziz5SwF2RAm+Rken21HIg==" + "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==", + "license": "BSD-3-Clause" }, "node_modules/@esbuild/aix-ppc64": { "version": "0.19.12", diff --git a/package.json b/package.json index d752f3b..bd1786f 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "typescript": "^5.3.2" }, "dependencies": { - "@dylibso/xtp-bindgen": "1.0.0-rc.11", + "@dylibso/xtp-bindgen": "1.0.0-rc.13", "ejs": "^3.1.10" } } diff --git a/src/index.ts b/src/index.ts index 260fff1..c7e0ef8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,62 +1,77 @@ import ejs from "ejs"; -import { getContext, helpers, Property } from "@dylibso/xtp-bindgen"; +import { + helpers, + getContext, + ObjectType, + EnumType, + ArrayType, + XtpNormalizedType, + XtpTyped +} from "@dylibso/xtp-bindgen" -function toPythonType(property: Property): string { - let tp +function pythonTypeName(s: string): string { + return helpers.snakeToPascalCase(s); +} + +function pythonFunctionName(s: string): string { + return helpers.camelToSnakeCase(s); +} + +function isOptional(type: String): boolean { + return type.startsWith('Optional[') +} - if (property.$ref) { - tp = property.$ref.name - } else { - switch (property.type) { - case "string": - if (property.format === "date-time") { - tp = "datetime" - } else { - tp = "str" - } - break - case "number": - // @ts-ignore - if (property.contentType === "application/json") { - tp = "str" - } else if (property.format === "float" || property.format === "double") { - tp = "float" - } else { - tp = "int" - } - break - case "integer": - // @ts-ignore - if (property.contentType === "application/json") { - tp = "str" - } else { - tp = "int" - } - break - case "boolean": - tp = "bool" - break - case "object": - tp = "dict" - break - case "array": - if (!property.items) { - tp = "list" - } else { - tp = `List[${toPythonType(property.items as Property)}]` - } - break - case "buffer": - tp = "bytes" - break - default: - throw new Error("Can't convert property to Python type: " + property.type); - } +function toPythonTypeX(type: XtpNormalizedType): string { + const opt = (t: string) => { + return type.nullable ? `Optional[${t}]` : t } + switch (type.kind) { + case 'string': + return opt('str'); + case 'int32': + return opt('int'); + case 'float': + return opt('float'); + case 'double': + return opt('float') + case 'byte': + return opt('byte'); + case 'date-time': + return opt("datetime"); + case 'boolean': + return opt('bool'); + case 'array': + const arrayType = type as ArrayType + return opt(`List[${toPythonTypeX(arrayType.elementType)}]`); + case 'buffer': + return opt('bytes'); + case 'map': + // TODO: improve typing of dicts + return opt('dict'); + case 'object': + return opt(pythonTypeName((type as ObjectType).name)); + case 'enum': + return opt(pythonTypeName((type as EnumType).name)); + default: + throw new Error("Can't convert XTP type to Python type: " + type) + } +} + + +function toPythonType(property: XtpTyped): string { + let t = toPythonTypeX(property.xtpType); + if (isOptional(t)) return t; + return `Optional[${t}]`; +} - if (!tp) throw new Error("Cant convert property to Python type: " + property.type) - if (!property.nullable && !property.required) return tp - return `Optional[${tp}]` +function toPythonParamType(property: XtpTyped): string { + let t = toPythonTypeX(property.xtpType); + // We need to represent bare int/float as a str in Python for now, + // there may be some updates to the encoder we can make to handle + // this case at some point + t = t.replace('int', 'str'); + t = t.replace('float', 'str'); + return t; } export function render() { @@ -65,6 +80,9 @@ export function render() { ...helpers, ...getContext(), toPythonType, + toPythonParamType, + pythonTypeName, + pythonFunctionName }; const output = ejs.render(tmpl, ctx); diff --git a/template/plugin/__init__.py.ejs b/template/plugin/__init__.py.ejs index d3e6dd9..ae0dcf9 100644 --- a/template/plugin/__init__.py.ejs +++ b/template/plugin/__init__.py.ejs @@ -1,10 +1,12 @@ # THIS FILE WAS GENERATED BY `xtp-python-bindgen`. DO NOT EDIT. +from typing import Optional, List # noqa: F401 +from datetime import datetime # noqa: F401 import extism # pyright: ignore import plugin <% if (Object.values(schema.schemas).length > 0){ %> -from pdk_types import <%- Object.values(schema.schemas).map(schema => schema.name).join(", ") %> # noqa: F401 +from pdk_types import <%- Object.values(schema.schemas).map(schema => pythonTypeName(schema.name)).join(", ") %> # noqa: F401 <% } %> # Imports @@ -13,13 +15,13 @@ from pdk_types import <%- Object.values(schema.schemas).map(schema => schema.nam <% if (hasComment(imp)) -%> #<%- imp.name %> <%- formatCommentBlock(imp.description, "# ") %> <% if (hasComment(imp.input)) { -%> -# It takes input of <%- toPythonType(imp.input) %> (<%- formatCommentLine(imp.input.description) %>) +# It takes input of <%- toPythonParamType(imp.input) %> (<%- formatCommentLine(imp.input.description) %>) <% } -%> <% if (hasComment(imp.output)) { -%> -# And it returns an output <%- toPythonType(imp.output) %> (<%- formatCommentLine(imp.output.description) %>) +# And it returns an output <%- toPythonParamType(imp.output) %> (<%- formatCommentLine(imp.output.description) %>) <% } -%> @extism.import_fn("extism:host/user", "<%- imp.name %>") -def <%- camelToSnakeCase(imp.name) %>(<% if (imp.input) { -%>input: <%- toPythonType(imp.input) %><%} -%>) <% if (imp.output) { %> -> <%- toPythonType(imp.output) %><% } %>: # pyright: ignore [reportReturnType] +def <%- pythonFunctionName(imp.name) %>(<% if (imp.input) { -%>input: <%- toPythonParamType(imp.input) %><%} -%>) <% if (imp.output) { %> -> <%- toPythonParamType(imp.output) %><% } %>: # pyright: ignore [reportReturnType] pass <% }) %> @@ -32,7 +34,7 @@ def <%- camelToSnakeCase(imp.name) %>(<% if (imp.input) { -%>input: <%- toPython <% } -%> @extism.plugin_fn def <%- ex.name %>(): - res = plugin.<%- camelToSnakeCase(ex.name) %>(<% if (ex.input) { %> extism.input(<%- toPythonType(ex.input) %>) <% } %>) + res = plugin.<%- pythonFunctionName(ex.name) %>(<% if (ex.input) { %> extism.input(<%- toPythonParamType(ex.input) %>) <% } %>) extism.output(res) <% }) %> diff --git a/template/plugin/pdk_imports.py.ejs b/template/plugin/pdk_imports.py.ejs index 9c5cb9d..59ac0f2 100644 --- a/template/plugin/pdk_imports.py.ejs +++ b/template/plugin/pdk_imports.py.ejs @@ -1,22 +1,24 @@ # THIS FILE WAS GENERATED BY `xtp-python-bindgen`. DO NOT EDIT. +from typing import Optional, List # noqa: F401 +from datetime import datetime # noqa: F401 import extism # noqa: F401 # pyright: ignore <% if (Object.values(schema.schemas).length > 0) { %> -from pdk_types import <%- Object.values(schema.schemas).map(schema => schema.name).join(", ") %> # noqa: F401 +from pdk_types import <%- Object.values(schema.schemas).map(schema => pythonTypeName(schema.name)).join(", ") %> # noqa: F401 <% } %> <% schema.imports.forEach(imp => { %> <% if (hasComment(imp)) -%> #<%- imp.name %> <%- formatCommentBlock(imp.description, "# ") %> <% if (hasComment(imp.input)) { -%> -# It takes input of <%- toPythonType(imp.input) %> (<%- formatCommentLine(imp.input.description) %>) +# It takes input of <%- toPythonParamType(imp.input) %> (<%- formatCommentLine(imp.input.description) %>) <% } -%> <% if (hasComment(imp.output)) { -%> -# And it returns an output <%- toPythonType(imp.output) %> (<%- formatCommentLine(imp.output.description) %>) +# And it returns an output <%- toPythonParamType(imp.output) %> (<%- formatCommentLine(imp.output.description) %>) <% } -%> @extism.import_fn("extism:host/user", "<%- imp.name %>") -def <%- camelToSnakeCase(imp.name) %>(<% if (imp.input) { -%>input: <%- toPythonType(imp.input) %><%} -%>) <% if (imp.output) { %> -> <%- toPythonType(imp.output) %><% } %>: # pyright: ignore [reportReturnType] +def <%- pythonFunctionName(imp.name) %>(<% if (imp.input) { -%>input: <%- toPythonParamType(imp.input) %><%} -%>) <% if (imp.output) { %> -> <%- toPythonParamType(imp.output) %><% } %>: # pyright: ignore [reportReturnType] pass <% }) %> diff --git a/template/plugin/pdk_types.py.ejs b/template/plugin/pdk_types.py.ejs index cdcd11e..5054b1e 100644 --- a/template/plugin/pdk_types.py.ejs +++ b/template/plugin/pdk_types.py.ejs @@ -10,27 +10,27 @@ import extism # noqa: F401 # pyright: ignore <% Object.values(schema.schemas).forEach(schema => { %> <% if (schema.enum) { %> -class <%- capitalize(schema.name) %>(Enum): +class <%- pythonTypeName(schema.name) %>(Enum): <% schema.enum.forEach(variant => { -%> - <%- capitalize(variant) %> = "<%- variant %>" + <%- pythonTypeName(variant) %> = "<%- variant %>" <% }) %> <% } else { %> @dataclass -class <%- capitalize(schema.name) %>(extism.Json): +class <%- pythonTypeName(schema.name) %>(extism.Json): <% schema.properties.forEach(p => { -%> -<% if (p.description) { -%> - # <%- formatCommentBlock(p.description, "# ") %> -<% } -%> -<% if (!p.nullable) {%> +<% if (!p.nullable || p.required) {%> + <% if (p.description) { -%> + # <%- formatCommentBlock(p.description, "# ") %> + <% } -%> <%- p.name %>: <%- toPythonType(p) %> <% } %> <% }) %> <% schema.properties.forEach(p => { -%> -<% if (p.description) { -%> - # <%- formatCommentBlock(p.description, "# ") %> -<% } -%> -<% if (p.nullable) {%> +<% if (p.nullable && !p.required) {%> + <% if (p.description) { -%> + # <%- formatCommentBlock(p.description, "# ") %> + <% } -%> <%- p.name %>: <%- toPythonType(p) %> = None <% } %> <% }) %> diff --git a/template/plugin/plugin.py.ejs b/template/plugin/plugin.py.ejs index 4afb7a2..720f83a 100644 --- a/template/plugin/plugin.py.ejs +++ b/template/plugin/plugin.py.ejs @@ -1,9 +1,12 @@ +from typing import Optional, List # noqa: F401 +from datetime import datetime # noqa: F401 import extism # noqa: F401 # pyright: ignore + <% if (Object.values(schema.schemas).length > 0) { %> -from pdk_types import <%- Object.values(schema.schemas).map(schema => schema.name).join(", ") %> # noqa: F401 +from pdk_types import <%- Object.values(schema.schemas).map(schema => pythonTypeName(schema.name)).join(", ") %> # noqa: F401 <% } %> <% if (schema.imports.length > 0) { %> -from pdk_imports import <%- schema.imports.map(schema => camelToSnakeCase(schema.name)).join(", ") %> # noqa: F401 +from pdk_imports import <%- schema.imports.map(schema => pythonFunctionName(schema.name)).join(", ") %> # noqa: F401 <% } %> from typing import List, Optional # noqa: F401 @@ -11,7 +14,7 @@ from typing import List, Optional # noqa: F401 <% if (hasComment(ex)) { -%> # <%- formatCommentBlock(ex.description, "# ") %> <% } -%> -def <%- camelToSnakeCase(ex.name) %>(<% if (ex.input) { %>input: <%- toPythonType(ex.input) %> <% } %>) <% if (ex.output) {%>-> <%- toPythonType(ex.output) %><%}%>: +def <%- pythonFunctionName(ex.name) %>(<% if (ex.input) { %>input: <%- toPythonParamType(ex.input) %> <% } %>) <% if (ex.output) {%>-> <%- toPythonParamType(ex.output) %><%}%>: <% if (featureFlags['stub-with-code-samples'] && codeSamples(ex, 'python').length > 0) { -%> <%- codeSamples(ex, 'python')[0].source %> <% } else { -%>