From 98290dca5377a2bd6a028e049e8a648e4ee28df8 Mon Sep 17 00:00:00 2001 From: Jarrod Overson Date: Tue, 9 Nov 2021 13:21:15 -0500 Subject: [PATCH] rust: combined components module with generated module, started normalizing around interface.json signature vs processing schemas in templates, added rust test project --- README.md | 17 +- package.json | 3 +- src/cli.ts | 2 +- src/common.ts | 41 +- src/languages/json/json.ts | 171 +-------- src/languages/rust/interface.ts | 5 +- .../rust/provider-component-module.ts | 53 --- src/languages/rust/provider-component.ts | 2 + src/languages/rust/provider-integration.ts | 22 +- src/languages/rust/wapc-component-module.ts | 53 --- src/languages/rust/wapc-component.ts | 2 + src/languages/rust/wapc-integration.ts | 21 +- src/languages/rust/wapc-lib.ts | 2 + ...ent-module.ts => wellknown-implementer.ts} | 4 +- src/languages/rust/wellknown-integration.ts | 51 --- src/process-widl-dir.ts | 166 ++++++++ templates/json/interface.hbs | 2 +- templates/rust/interface.hbs | 46 +-- .../partials/common/component-imports.hbs | 3 + .../rust/partials/common/expand-type.hbs | 30 +- .../rust/partials/common/native-inputs.hbs | 28 +- .../rust/partials/common/native-outputs.hbs | 36 +- .../common/native-type-definition.hbs | 20 - .../partials/common/provider-signature.hbs | 16 + .../partials/common/struct-definition.hbs | 7 + .../rust/partials/common/type-signature.hbs | 75 ++-- templates/rust/partials/common/types.hbs | 12 + .../rust/partials/wapc-integration/Inputs.hbs | 35 -- .../partials/wapc-integration/Outputs.hbs | 50 --- .../wapc-integration/TypeDefinition.hbs | 19 - .../partials/wapc-integration/wasm-inputs.hbs | 35 ++ .../wapc-integration/wasm-outputs.hbs | 50 +++ .../component.hbs | 4 +- templates/rust/provider-component-module.hbs | 7 - templates/rust/provider-component.hbs | 2 +- templates/rust/provider-integration.hbs | 127 +++---- templates/rust/wapc-component-module.hbs | 7 - templates/rust/wapc-component.hbs | 2 +- templates/rust/wapc-integration.hbs | 70 ++-- templates/rust/wapc-lib.hbs | 1 - templates/rust/wellknown-component-module.hbs | 7 - ...egration.hbs => wellknown-implementer.hbs} | 31 +- test/fixtures/hello-world.widl | 2 +- test/fixtures/rust-project/.cargo/config.toml | 7 + .../rust-project/.github/workflows/build.yml | 23 ++ test/fixtures/rust-project/.gitignore | 15 + .../rust-project/.vscode/extensions.json | 3 + .../fixtures/rust-project/.vscode/launch.json | 28 ++ .../rust-project/.vscode/settings.json | 9 + test/fixtures/rust-project/.vscode/tasks.json | 29 ++ test/fixtures/rust-project/Cargo.toml | 20 + test/fixtures/rust-project/LICENSE | 29 ++ test/fixtures/rust-project/Makefile | 123 ++++++ test/fixtures/rust-project/README.md | 24 ++ test/fixtures/rust-project/interface.json | 83 ++++ .../fixtures/rust-project/rust-toolchain.toml | 3 + test/fixtures/rust-project/schemas/add.widl | 13 + .../rust-project/schemas/hello-world.widl | 13 + .../rust-project/schemas/http-request.widl | 12 + .../rust-project/schemas/include/types.widl | 10 + test/fixtures/rust-project/src/components.rs | 354 ++++++++++++++++++ .../rust-project/src/components/add.rs | 5 + .../src/components/hello_world.rs | 5 + .../rust-project/src/components/http.rs | 5 + .../src/components/http_request.rs | 5 + .../src/components/my_component.rs | 5 + test/fixtures/rust-project/src/lib.rs | 1 + test/json.test.ts | 2 +- 68 files changed, 1385 insertions(+), 780 deletions(-) delete mode 100644 src/languages/rust/provider-component-module.ts delete mode 100644 src/languages/rust/wapc-component-module.ts rename src/languages/rust/{wellknown-component-module.ts => wellknown-implementer.ts} (92%) delete mode 100644 src/languages/rust/wellknown-integration.ts create mode 100644 src/process-widl-dir.ts create mode 100644 templates/rust/partials/common/component-imports.hbs delete mode 100644 templates/rust/partials/common/native-type-definition.hbs create mode 100644 templates/rust/partials/common/provider-signature.hbs create mode 100644 templates/rust/partials/common/struct-definition.hbs create mode 100644 templates/rust/partials/common/types.hbs delete mode 100644 templates/rust/partials/wapc-integration/Inputs.hbs delete mode 100644 templates/rust/partials/wapc-integration/Outputs.hbs delete mode 100644 templates/rust/partials/wapc-integration/TypeDefinition.hbs create mode 100644 templates/rust/partials/wapc-integration/wasm-inputs.hbs create mode 100644 templates/rust/partials/wapc-integration/wasm-outputs.hbs rename templates/rust/partials/{wellknown-integration => wellknown-implementer}/component.hbs (94%) delete mode 100644 templates/rust/provider-component-module.hbs delete mode 100644 templates/rust/wapc-component-module.hbs delete mode 100644 templates/rust/wellknown-component-module.hbs rename templates/rust/{wellknown-integration.hbs => wellknown-implementer.hbs} (62%) create mode 100644 test/fixtures/rust-project/.cargo/config.toml create mode 100644 test/fixtures/rust-project/.github/workflows/build.yml create mode 100644 test/fixtures/rust-project/.gitignore create mode 100644 test/fixtures/rust-project/.vscode/extensions.json create mode 100644 test/fixtures/rust-project/.vscode/launch.json create mode 100644 test/fixtures/rust-project/.vscode/settings.json create mode 100644 test/fixtures/rust-project/.vscode/tasks.json create mode 100644 test/fixtures/rust-project/Cargo.toml create mode 100644 test/fixtures/rust-project/LICENSE create mode 100644 test/fixtures/rust-project/Makefile create mode 100644 test/fixtures/rust-project/README.md create mode 100644 test/fixtures/rust-project/interface.json create mode 100644 test/fixtures/rust-project/rust-toolchain.toml create mode 100644 test/fixtures/rust-project/schemas/add.widl create mode 100644 test/fixtures/rust-project/schemas/hello-world.widl create mode 100644 test/fixtures/rust-project/schemas/http-request.widl create mode 100644 test/fixtures/rust-project/schemas/include/types.widl create mode 100644 test/fixtures/rust-project/src/components.rs create mode 100644 test/fixtures/rust-project/src/components/add.rs create mode 100644 test/fixtures/rust-project/src/components/hello_world.rs create mode 100644 test/fixtures/rust-project/src/components/http.rs create mode 100644 test/fixtures/rust-project/src/components/http_request.rs create mode 100644 test/fixtures/rust-project/src/components/my_component.rs create mode 100644 test/fixtures/rust-project/src/lib.rs diff --git a/README.md b/README.md index b847988..445b313 100644 --- a/README.md +++ b/README.md @@ -19,16 +19,13 @@ vino-codegen rust Generate Rust code from a WIDL schema Commands: - vino-codegen rust interface [options] Generate source code for well-known interfaces - vino-codegen rust provider-component-module [options] Generate root native provider components module - vino-codegen rust provider-component [options] Generate boilerplate for native provider components - vino-codegen rust provider-integration [options] Generate the Vino integration code for all component schemas - vino-codegen rust wapc-component-module [options] Generate root native provider components module - vino-codegen rust wapc-component [options] Generate boilerplate for WaPC components - vino-codegen rust wapc-integration [options] Generate the Vino & WaPC integration code for all component schemas - vino-codegen rust wapc-lib Generate the boilerplate lib.rs for WaPC components - vino-codegen rust wellknown-component-module [options] Generate the Vino integration code for well-known interface schemas - vino-codegen rust wellknown-integration [options] Generate the Vino integration code for well-known interface schemas + vino-codegen rust interface [options] Generate source code for well-known interfaces + vino-codegen rust provider-component [options] Generate boilerplate for native provider components + vino-codegen rust provider-integration [options] Generate the Vino integration code for all component schemas + vino-codegen rust wapc-component [options] Generate boilerplate for WaPC components + vino-codegen rust wapc-integration [options] Generate the Vino & WaPC integration code for all component schemas + vino-codegen rust wapc-lib Generate the boilerplate lib.rs for WaPC components + vino-codegen rust wellknown-implementer [options] Generate the Vino integration code for well-known interface schemas Options: --version Show version number [boolean] diff --git a/package.json b/package.json index 3363b2c..9891f31 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "format": "prettier --write 'src/**/*.ts' 'test/**/*.ts'", "watch": "npm run clean && tsc -w --declaration", "test:unit": "mocha", - "test": "npm run lint && npm run test:unit" + "test:rust": "cd test/fixtures/rust-project && make clean && make codegen && cargo check", + "test": "npm run lint && npm run test:unit && npm run test:rust" }, "bin": { "vino-codegen": "./dist/src/cli.js" diff --git a/src/cli.ts b/src/cli.ts index cfe3596..facf2cb 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -15,7 +15,7 @@ try { debug('Done processing command'); } catch (e) { debug('Error %o', e); - console.error(`Error running task : ${e.message}`); + console.error(`Error running task : ${e}`); process.exit(1); } debug('Done with main'); diff --git a/src/common.ts b/src/common.ts index de01a91..9b236cf 100644 --- a/src/common.ts +++ b/src/common.ts @@ -15,13 +15,10 @@ export enum LANGUAGE { export enum CODEGEN_TYPE { ProviderComponent = 'provider-component', - ProviderComponentModule = 'provider-component-module', ProviderIntegration = 'provider-integration', - WellKnownIntegration = 'wellknown-integration', - WellKnownComponentModule = 'wellknown-component-module', + WellKnownImplementer = 'wellknown-implementer', Interface = 'interface', WapcComponent = 'wapc-component', - WapcComponentModule = 'wapc-component-module', WapcIntegration = 'wapc-integration', WapcLib = 'wapc-lib', } @@ -106,6 +103,42 @@ export function registerTypePartials(language: LANGUAGE, type: CODEGEN_TYPE | WI } } +export interface HelperMap { + [name: string]: Handlebars.HelperDelegate; +} + +export function registerLanguageHelpers(lang: LANGUAGE): void { + handlebars.registerHelper('ifEmpty', function (this: any, context: unknown, options): string { + let isEmpty = false; + if (context === undefined || context === null) { + isEmpty = true; + } else if (typeof context === 'string' && context.length === 0) { + isEmpty = true; + } else if (Array.isArray(context) && context.length === 0) { + isEmpty = true; + } else if (context && typeof context === 'object' && Object.keys(context).length === 0) { + isEmpty = true; + } else { + isEmpty = false; + } + return isEmpty ? options.fn(this) : options.inverse(this); + }); + switch (lang) { + case LANGUAGE.Rust: + { + handlebars.registerHelper('refToModulePath', function (context: string): string { + if (context) { + return context.substr(1).split('/').slice(1).join('::'); + } else { + throw new Error(`Called refToModulePath with invalid context: ${context}`); + } + }); + } + break; + default: + } +} + export function registerCommonPartials(language: LANGUAGE): void { const relativeDir = path.join(language, 'partials', 'common'); const dir = path.join(findroot(__dirname), 'templates', relativeDir); diff --git a/src/languages/json/json.ts b/src/languages/json/json.ts index 78b4076..e78fd2a 100644 --- a/src/languages/json/json.ts +++ b/src/languages/json/json.ts @@ -4,39 +4,15 @@ import { LANGUAGE, registerTypePartials, JSON_TYPE, - readFile, outputOpts, widlOpts, CommonOutputOptions, CommonWidlOptions, } from '../../common'; -import path from 'path'; -import fs from 'fs'; -import { parse } from '@wapc/widl'; -import { - AbstractNode, - Annotation, - Definition, - Document, - Kind, - ListType, - MapType, - Named, - NamespaceDefinition, - Optional, - Type, - TypeDefinition, -} from '@wapc/widl/ast'; import { registerHelpers } from 'widl-template'; -import { - ComponentSignature, - isWidlType, - ProviderSignature, - StructSignature, - TypeMap, - TypeSignature, -} from '../../types'; + +import { processDir } from '../../process-widl-dir'; const LANG = LANGUAGE.JSON; const TYPE = JSON_TYPE.Interface; @@ -65,122 +41,6 @@ export interface Arguments extends CommonOutputOptions, CommonWidlOptions { schema_dir: string; } -function isType(def: Definition): def is TypeDefinition { - return def.isKind(Kind.TypeDefinition); -} - -interface HasName extends AbstractNode { - name: { value: string }; -} - -function findByName(defs: T[], name: string): T | undefined { - return defs.find(def => def.name.value === name); -} - -function reduceTypeDefinition(def: TypeDefinition): StructSignature { - const fields: Record = {}; - for (const field of def.fields) { - fields[field.name.value] = reduceType(field.type, field.annotations); - } - - return { - name: def.name.value, - fields, - }; -} - -function getAnnotation(name: string, annotations: Annotation[]): Annotation | undefined { - const result = annotations.filter(a => a.name.value == name)[0]; - return result; -} - -function reduceType(type: Type, annotations: Annotation[] = []): TypeSignature { - switch (type.getKind()) { - case Kind.Named: { - const t = type as Named; - const name = t.name; - if (isWidlType(name.value)) { - return { type: name.value }; - } else { - const link = getAnnotation('provider', annotations); - if (name.value === 'link') { - if (link) { - const provider = link.arguments[0]; - return { - type: 'link', - provider: provider.value.getValue(), - }; - } else { - return { - type: 'link', - }; - } - } else { - return { type: 'ref', ref: `#/types/${name.value}` }; - } - } - } - case Kind.MapType: { - const t = type as MapType; - return { - type: 'map', - key: reduceType(t.keyType, annotations), - value: reduceType(t.valueType, annotations), - }; - } - case Kind.ListType: { - const t = type as ListType; - return { - type: 'list', - element: reduceType(t.type, annotations), - }; - } - case Kind.Optional: { - const t = type as Optional; - return { - type: 'optional', - option: reduceType(t.type, annotations), - }; - } - } - throw new Error(`Unhandled type: ${type.getKind()}`); -} - -function interpret(doc: Document): [ComponentSignature, Record] { - const types = doc.definitions.filter(isType); - const input_def = findByName(types, 'Inputs'); - const output_def = findByName(types, 'Outputs'); - const namespace = doc.definitions.find(def => def.isKind(Kind.NamespaceDefinition)); - if (!namespace) throw new Error('Component schemas must define a namespace to use as the component name'); - if (!input_def) throw new Error('Component schemas must include a type definition named "Inputs"'); - if (!output_def) throw new Error('Component schemas must include a type definition named "Outputs"'); - - const inputs: TypeMap = Object.fromEntries( - input_def.fields.map(field => { - return [field.name.value, reduceType(field.type, field.annotations)]; - }), - ); - - const outputs = Object.fromEntries( - output_def.fields.map(field => { - return [field.name.value, reduceType(field.type, field.annotations)]; - }), - ); - - const component: ComponentSignature = { - name: (namespace as NamespaceDefinition).name.value, - inputs, - outputs, - }; - - const typeSignatures = Object.fromEntries( - types - .filter(t => t.name.value !== 'Inputs' && t.name.value !== 'Outputs') - .map(t => [t.name.value, reduceTypeDefinition(t)]), - ); - return [component, typeSignatures]; -} - export function handler(args: Arguments): void { registerTypePartials(LANG, TYPE); const options = { @@ -188,32 +48,7 @@ export function handler(args: Arguments): void { }; registerHelpers(options); - const files = fs.readdirSync(args.schema_dir).filter(path => path.endsWith('.widl')); - - const components: Record = {}; - let types: Record = {}; - - const resolver = (location: string) => { - const pathParts = location.split('/'); - const importPath = path.join(args.schema_dir, ...pathParts); - const src = readFile(importPath); - return src; - }; - - for (const file of files) { - const widlSrc = readFile(path.join(args.schema_dir, file)); - const tree = parse(widlSrc, resolver); - const [component, additionalTypes] = interpret(tree); - types = Object.assign(types, additionalTypes); - - components[component.name] = component; - } - - const providerSignature: ProviderSignature = { - name: args.name, - types, - components, - }; + const providerSignature = processDir(args.name, args.schema_dir); const generated = JSON.stringify(providerSignature, null, 2); diff --git a/src/languages/rust/interface.ts b/src/languages/rust/interface.ts index fcfaf83..917f82a 100644 --- a/src/languages/rust/interface.ts +++ b/src/languages/rust/interface.ts @@ -15,6 +15,7 @@ import { } from '../../common'; import path from 'path'; import fs from 'fs'; +import { processDir } from '../../process-widl-dir'; const LANG = LANGUAGE.Rust; const TYPE = CODEGEN_TYPE.Interface; @@ -54,7 +55,7 @@ export function handler(args: Arguments): void { }); const template = handlebars.compile(getTemplate(LANG, TYPE)); - const generated = template({ schemas }); - + const iface = processDir('', args.schema_dir); + const generated = template({ schemas, interface: iface }); commitOutput(generated, args.output, { force: args.force, silent: args.silent }); } diff --git a/src/languages/rust/provider-component-module.ts b/src/languages/rust/provider-component-module.ts deleted file mode 100644 index d50c4fe..0000000 --- a/src/languages/rust/provider-component-module.ts +++ /dev/null @@ -1,53 +0,0 @@ -import yargs from 'yargs'; -import { handlebars, registerHelpers } from 'widl-template'; -import { - CODEGEN_TYPE, - getTemplate, - commitOutput, - LANGUAGE, - registerTypePartials, - CommonOutputOptions, - outputOpts, - CommonWidlOptions, - normalizeFilename, -} from '../../common'; -import fs from 'fs'; - -const LANG = LANGUAGE.Rust; -const TYPE = CODEGEN_TYPE.ProviderComponentModule; - -export const command = `${TYPE} [options]`; -export const desc = 'Generate root native provider components module'; - -export const builder = (yargs: yargs.Argv): yargs.Argv => { - return yargs - .positional('schema_dir', { - demandOption: true, - type: 'string', - description: 'The directory that holds your component schemas', - }) - .options(outputOpts({})) - .example(`${LANG} ${TYPE} schemas/`, 'Prints boilerplate components.rs to STDOUT'); -}; - -interface Arguments extends CommonWidlOptions, CommonOutputOptions { - schema_dir: string; -} - -export function handler(args: Arguments): void { - registerTypePartials(LANG, TYPE); - - const options = { - root: args.root, - }; - registerHelpers(options); - - const files = fs.readdirSync(args.schema_dir).filter(path => path.endsWith('.widl')); - - const schemas = files.map(normalizeFilename); - - const template = handlebars.compile(getTemplate(LANG, TYPE)); - const generated = template({ schemas }); - - commitOutput(generated, args.output, { force: args.force, silent: args.silent }); -} diff --git a/src/languages/rust/provider-component.ts b/src/languages/rust/provider-component.ts index 882ef93..16b4589 100644 --- a/src/languages/rust/provider-component.ts +++ b/src/languages/rust/provider-component.ts @@ -9,6 +9,7 @@ import { CommonOutputOptions, outputOpts, normalizeFilename, + registerLanguageHelpers, } from '../../common'; const LANG = LANGUAGE.Rust; @@ -35,6 +36,7 @@ interface Arguments extends CommonOutputOptions { export function handler(args: Arguments): void { registerTypePartials(LANG, TYPE); + registerLanguageHelpers(LANG); registerHelpers(); diff --git a/src/languages/rust/provider-integration.ts b/src/languages/rust/provider-integration.ts index 046377d..d4d3890 100644 --- a/src/languages/rust/provider-integration.ts +++ b/src/languages/rust/provider-integration.ts @@ -1,20 +1,19 @@ import yargs from 'yargs'; -import { handlebars, parseWidl, registerHelpers } from 'widl-template'; +import { handlebars, registerHelpers } from 'widl-template'; import { CODEGEN_TYPE, getTemplate, commitOutput, LANGUAGE, - readFile, registerTypePartials, widlOpts, CommonWidlOptions, CommonOutputOptions, outputOpts, - normalizeFilename, + registerLanguageHelpers, } from '../../common'; -import path from 'path'; -import fs from 'fs'; + +import { processDir } from '../../process-widl-dir'; const LANG = LANGUAGE.Rust; const TYPE = CODEGEN_TYPE.ProviderIntegration; @@ -39,22 +38,15 @@ interface Arguments extends CommonWidlOptions, CommonOutputOptions { export function handler(args: Arguments): void { registerTypePartials(LANG, TYPE); + registerLanguageHelpers(LANG); const options = { root: args.root, }; registerHelpers(options); - - const files = fs.readdirSync(args.schema_dir).filter(path => path.endsWith('.widl')); - - const schemas = files.map(file => { - const widlSrc = readFile(path.join(args.schema_dir, file)); - const tree = parseWidl(widlSrc); - return { file: normalizeFilename(file), document: tree }; - }); - const template = handlebars.compile(getTemplate(LANG, TYPE)); - const generated = template({ schemas }); + const iface = processDir('', args.schema_dir); + const generated = template({ interface: iface }); commitOutput(generated, args.output, { force: args.force, silent: args.silent }); } diff --git a/src/languages/rust/wapc-component-module.ts b/src/languages/rust/wapc-component-module.ts deleted file mode 100644 index 656a971..0000000 --- a/src/languages/rust/wapc-component-module.ts +++ /dev/null @@ -1,53 +0,0 @@ -import yargs from 'yargs'; -import { handlebars, registerHelpers } from 'widl-template'; -import { - CODEGEN_TYPE, - getTemplate, - commitOutput, - LANGUAGE, - registerTypePartials, - CommonOutputOptions, - outputOpts, - CommonWidlOptions, - normalizeFilename, -} from '../../common'; -import fs from 'fs'; - -const LANG = LANGUAGE.Rust; -const TYPE = CODEGEN_TYPE.WapcComponentModule; - -export const command = `${TYPE} [options]`; -export const desc = 'Generate root native provider components module'; - -export const builder = (yargs: yargs.Argv): yargs.Argv => { - return yargs - .positional('schema_dir', { - demandOption: true, - type: 'string', - description: 'The directory that holds your component schemas', - }) - .options(outputOpts({})) - .example(`${LANG} ${TYPE} schemas/`, 'Prints boilerplate components.rs to STDOUT'); -}; - -interface Arguments extends CommonWidlOptions, CommonOutputOptions { - schema_dir: string; -} - -export function handler(args: Arguments): void { - registerTypePartials(LANG, TYPE); - - const options = { - root: args.root, - }; - registerHelpers(options); - - const files = fs.readdirSync(args.schema_dir).filter(path => path.endsWith('.widl')); - - const schemas = files.map(normalizeFilename); - - const template = handlebars.compile(getTemplate(LANG, TYPE)); - const generated = template({ schemas }); - - commitOutput(generated, args.output, { force: args.force, silent: args.silent }); -} diff --git a/src/languages/rust/wapc-component.ts b/src/languages/rust/wapc-component.ts index 3e4ebec..9276dd3 100644 --- a/src/languages/rust/wapc-component.ts +++ b/src/languages/rust/wapc-component.ts @@ -9,6 +9,7 @@ import { CommonOutputOptions, outputOpts, normalizeFilename, + registerLanguageHelpers, } from '../../common'; const LANG = LANGUAGE.Rust; @@ -35,6 +36,7 @@ interface Arguments extends CommonOutputOptions { export function handler(args: Arguments): void { registerTypePartials(LANG, TYPE); + registerLanguageHelpers(LANG); registerHelpers(); diff --git a/src/languages/rust/wapc-integration.ts b/src/languages/rust/wapc-integration.ts index d2c7a3e..e73d728 100644 --- a/src/languages/rust/wapc-integration.ts +++ b/src/languages/rust/wapc-integration.ts @@ -1,20 +1,19 @@ import yargs from 'yargs'; -import { handlebars, parseWidl, registerHelpers } from 'widl-template'; +import { handlebars, registerHelpers } from 'widl-template'; import { CODEGEN_TYPE, getTemplate, commitOutput, LANGUAGE, - readFile, registerTypePartials, widlOpts, CommonWidlOptions, CommonOutputOptions, outputOpts, - normalizeFilename, + registerLanguageHelpers, } from '../../common'; -import path from 'path'; -import fs from 'fs'; + +import { processDir } from '../../process-widl-dir'; const LANG = LANGUAGE.Rust; const TYPE = CODEGEN_TYPE.WapcIntegration; @@ -39,22 +38,16 @@ interface Arguments extends CommonWidlOptions, CommonOutputOptions { export function handler(args: Arguments): void { registerTypePartials(LANG, TYPE); + registerLanguageHelpers(LANG); const options = { root: args.root, }; registerHelpers(options); - const files = fs.readdirSync(args.schema_dir).filter(path => path.endsWith('.widl')); - - const schemas = files.map(file => { - const widlSrc = readFile(path.join(args.schema_dir, file)); - const tree = parseWidl(widlSrc); - return { file: normalizeFilename(file), document: tree }; - }); - const template = handlebars.compile(getTemplate(LANG, TYPE)); - const generated = template({ schemas }); + const iface = processDir('', args.schema_dir); + const generated = template({ interface: iface }); commitOutput(generated, args.output, { force: args.force, silent: args.silent }); } diff --git a/src/languages/rust/wapc-lib.ts b/src/languages/rust/wapc-lib.ts index 979e3e2..2bded85 100644 --- a/src/languages/rust/wapc-lib.ts +++ b/src/languages/rust/wapc-lib.ts @@ -9,6 +9,7 @@ import { CommonOutputOptions, outputOpts, CommonWidlOptions, + registerLanguageHelpers, } from '../../common'; const LANG = LANGUAGE.Rust; @@ -25,6 +26,7 @@ interface Arguments extends CommonWidlOptions, CommonOutputOptions {} export function handler(args: Arguments): void { registerTypePartials(LANG, TYPE); + registerLanguageHelpers(LANG); const options = { root: args.root, diff --git a/src/languages/rust/wellknown-component-module.ts b/src/languages/rust/wellknown-implementer.ts similarity index 92% rename from src/languages/rust/wellknown-component-module.ts rename to src/languages/rust/wellknown-implementer.ts index 06c1115..d53865c 100644 --- a/src/languages/rust/wellknown-component-module.ts +++ b/src/languages/rust/wellknown-implementer.ts @@ -11,10 +11,11 @@ import { CommonWidlOptions, CommonOutputOptions, outputOpts, + registerLanguageHelpers, } from '../../common'; const LANG = LANGUAGE.Rust; -const TYPE = CODEGEN_TYPE.WellKnownComponentModule; +const TYPE = CODEGEN_TYPE.WellKnownImplementer; export const command = `${TYPE} [options]`; export const desc = 'Generate the Vino integration code for well-known interface schemas'; @@ -36,6 +37,7 @@ interface Arguments extends CommonWidlOptions, CommonOutputOptions { export function handler(args: Arguments): void { registerTypePartials(LANG, TYPE); + registerLanguageHelpers(LANG); const options = { root: args.root, diff --git a/src/languages/rust/wellknown-integration.ts b/src/languages/rust/wellknown-integration.ts deleted file mode 100644 index 080b1b9..0000000 --- a/src/languages/rust/wellknown-integration.ts +++ /dev/null @@ -1,51 +0,0 @@ -import yargs from 'yargs'; -import { handlebars, registerHelpers } from 'widl-template'; -import { - CODEGEN_TYPE, - getTemplate, - commitOutput, - LANGUAGE, - readFile, - registerTypePartials, - widlOpts, - CommonWidlOptions, - CommonOutputOptions, - outputOpts, -} from '../../common'; - -const LANG = LANGUAGE.Rust; -const TYPE = CODEGEN_TYPE.WellKnownIntegration; - -export const command = `${TYPE} [options]`; -export const desc = 'Generate the Vino integration code for well-known interface schemas'; - -export const builder = (yargs: yargs.Argv): yargs.Argv => { - return yargs - .positional('interface', { - demandOption: true, - type: 'string', - description: 'Path to well-known interface schema (JSON)', - }) - .options(outputOpts(widlOpts({}))) - .example(`rust ${TYPE} interface.json`, 'Prints generated code to STDOUT'); -}; - -interface Arguments extends CommonWidlOptions, CommonOutputOptions { - interface: string; -} - -export function handler(args: Arguments): void { - registerTypePartials(LANG, TYPE); - - const options = { - root: args.root, - }; - registerHelpers(options); - const interfaceJson = readFile(args.interface); - const iface = JSON.parse(interfaceJson); - - const template = handlebars.compile(getTemplate(LANG, TYPE)); - const generated = template({ interface: iface }); - - commitOutput(generated, args.output, { force: args.force, silent: args.silent }); -} diff --git a/src/process-widl-dir.ts b/src/process-widl-dir.ts new file mode 100644 index 0000000..fd43211 --- /dev/null +++ b/src/process-widl-dir.ts @@ -0,0 +1,166 @@ +import { readFile } from './common'; +import path from 'path'; +import fs from 'fs'; +import { parse } from '@wapc/widl'; +import { + AbstractNode, + Annotation, + Definition, + Document, + Kind, + ListType, + MapType, + Named, + NamespaceDefinition, + Optional, + Type, + TypeDefinition, +} from '@wapc/widl/ast'; + +import { ComponentSignature, isWidlType, ProviderSignature, StructSignature, TypeMap, TypeSignature } from './types'; + +export function processDir(name: string, dir: string): ProviderSignature { + const files = fs.readdirSync(dir).filter(path => path.endsWith('.widl')); + + const components: Record = {}; + let types: Record = {}; + + const resolver = (location: string) => { + const pathParts = location.split('/'); + const importPath = path.join(dir, ...pathParts); + const src = readFile(importPath); + return src; + }; + + for (const file of files) { + const widlSrc = readFile(path.join(dir, file)); + const tree = parse(widlSrc, resolver); + const [component, additionalTypes] = interpret(tree); + types = Object.assign(types, additionalTypes); + + components[component.name] = component; + } + + const providerSignature: ProviderSignature = { + name, + types, + components, + }; + return providerSignature; +} + +function getAnnotation(name: string, annotations: Annotation[]): Annotation | undefined { + const result = annotations.filter(a => a.name.value == name)[0]; + return result; +} + +function reduceType(type: Type, annotations: Annotation[] = []): TypeSignature { + switch (type.getKind()) { + case Kind.Named: { + const t = type as Named; + const name = t.name; + if (isWidlType(name.value)) { + return { type: name.value }; + } else { + const link = getAnnotation('provider', annotations); + if (name.value === 'link') { + if (link) { + const provider = link.arguments[0]; + return { + type: 'link', + provider: provider.value.getValue(), + }; + } else { + return { + type: 'link', + }; + } + } else { + return { type: 'ref', ref: `#/types/${name.value}` }; + } + } + } + case Kind.MapType: { + const t = type as MapType; + return { + type: 'map', + key: reduceType(t.keyType, annotations), + value: reduceType(t.valueType, annotations), + }; + } + case Kind.ListType: { + const t = type as ListType; + return { + type: 'list', + element: reduceType(t.type, annotations), + }; + } + case Kind.Optional: { + const t = type as Optional; + return { + type: 'optional', + option: reduceType(t.type, annotations), + }; + } + } + throw new Error(`Unhandled type: ${type.getKind()}`); +} + +function interpret(doc: Document): [ComponentSignature, Record] { + const types = doc.definitions.filter(isType); + const input_def = findByName(types, 'Inputs'); + const output_def = findByName(types, 'Outputs'); + const namespace = doc.definitions.find(def => def.isKind(Kind.NamespaceDefinition)); + if (!namespace) throw new Error('Component schemas must define a namespace to use as the component name'); + if (!input_def) throw new Error('Component schemas must include a type definition named "Inputs"'); + if (!output_def) throw new Error('Component schemas must include a type definition named "Outputs"'); + + const inputs: TypeMap = Object.fromEntries( + input_def.fields.map(field => { + return [field.name.value, reduceType(field.type, field.annotations)]; + }), + ); + + const outputs = Object.fromEntries( + output_def.fields.map(field => { + return [field.name.value, reduceType(field.type, field.annotations)]; + }), + ); + + const component: ComponentSignature = { + name: (namespace as NamespaceDefinition).name.value, + inputs, + outputs, + }; + + const typeSignatures = Object.fromEntries( + types + .filter(t => t.name.value !== 'Inputs' && t.name.value !== 'Outputs') + .map(t => [t.name.value, reduceTypeDefinition(t)]), + ); + return [component, typeSignatures]; +} + +function isType(def: Definition): def is TypeDefinition { + return def.isKind(Kind.TypeDefinition); +} + +interface HasName extends AbstractNode { + name: { value: string }; +} + +function findByName(defs: T[], name: string): T | undefined { + return defs.find(def => def.name.value === name); +} + +function reduceTypeDefinition(def: TypeDefinition): StructSignature { + const fields: Record = {}; + for (const field of def.fields) { + fields[field.name.value] = reduceType(field.type, field.annotations); + } + + return { + name: def.name.value, + fields, + }; +} diff --git a/templates/json/interface.hbs b/templates/json/interface.hbs index 76abfef..8588acb 100644 --- a/templates/json/interface.hbs +++ b/templates/json/interface.hbs @@ -1 +1 @@ -{{json}} \ No newline at end of file +this template intentionally unused. \ No newline at end of file diff --git a/templates/rust/interface.hbs b/templates/rust/interface.hbs index 55900ab..197a751 100644 --- a/templates/rust/interface.hbs +++ b/templates/rust/interface.hbs @@ -2,45 +2,33 @@ ***** This file is generated, do not edit ***** ***********************************************/ -{{#each schemas}} - pub mod {{snakeCase file.unhyphenated}} { - use serde::{ - Deserialize, - Serialize, - }; - use std::collections::HashMap; +{{#each types}} +#[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize, Clone)] +pub struct {{pascalCase name}} { + {{#each fields }} + #[serde(rename = "{{name}}")] + pub {{snakeCase @key}}: {{> expand-type . }}, + {{/each}} +} +{{/each}} - #[cfg(feature = "native")] - pub use vino_provider::native::prelude::*; - #[cfg(feature = "wasm")] - pub use vino_provider::wasm::prelude::*; +{{#each interface.components}} + pub mod {{snakeCase name}} { - {{#with document}} + use std::collections::HashMap; + + pub use vino_provider::prelude::*; #[cfg(any(feature = "native", feature = "wasm"))] pub fn signature() -> ComponentSignature { ComponentSignature { - name: "{{namespace.name.value}}".to_owned(), + name: "{{name}}".to_owned(), inputs : inputs_list().into(), outputs : outputs_list().into(), } } - {{#each definitions}} - {{#switch kind}} - {{#case "NamespaceDefinition"}}{{/case}} - {{#case "TypeDefinition"}} - {{#switch name.value}} - {{> native-type-definition .}} - {{/switch}} - {{/case}} - {{#default}} - {{debug .}} - {{panic "WIDL Node not yet handled"}} - {{/default}} - {{/switch}} - {{/each}} - {{/with}} - + {{> native-inputs inputs }} + {{> native-outputs outputs }} } {{/each}} \ No newline at end of file diff --git a/templates/rust/partials/common/component-imports.hbs b/templates/rust/partials/common/component-imports.hbs new file mode 100644 index 0000000..1a5e36c --- /dev/null +++ b/templates/rust/partials/common/component-imports.hbs @@ -0,0 +1,3 @@ +{{#each interface.components}} +pub mod {{snakeCase name}}; +{{/each}} diff --git a/templates/rust/partials/common/expand-type.hbs b/templates/rust/partials/common/expand-type.hbs index 959e21a..619f3cc 100644 --- a/templates/rust/partials/common/expand-type.hbs +++ b/templates/rust/partials/common/expand-type.hbs @@ -1,24 +1,18 @@ -{{#switch kind ~}} - {{#case "Named"}} - {{#switch name.value}} - {{#case "string"}}String{{/case}} - {{#case "bytes"}}Vec{{/case}} - {{#case "raw"}}RawPacket{{/case}} - {{#case "link"}}ProviderLink{{/case}} - {{#default}}{{name.value}}{{/default}} - {{/switch}} +{{#switch type ~}} + {{#case "ref"}}super::{{refToModulePath ref}}{{/case}} + {{#case "string"}}String{{/case}} + {{#case "bytes"}}Vec{{/case}} + {{#case "raw"}}RawPacket{{/case}} + {{#case "link"}}ProviderLink{{/case}} + {{#case "map"}} + std::collections::HashMap<{{> expand-type key}}, {{> expand-type value}}> {{/case}} - {{#case "MapType"}} - std::collections::HashMap<{{> expand-type keyType}}, {{> expand-type valueType}}> + {{#case "list"}} + Vec<{{> expand-type element}}> {{/case}} - {{#case "ListType"}} - Vec<{{> expand-type type}}> - {{/case}} - {{#case "Optional"}} + {{#case "optional"}} Option<{{>expand-type type}}> {{/case}} - {{#default}} - unknown - {{/default}} + {{#default}}{{type}}{{/default}} {{~/switch}} diff --git a/templates/rust/partials/common/native-inputs.hbs b/templates/rust/partials/common/native-inputs.hbs index 6dab80b..fe6032a 100644 --- a/templates/rust/partials/common/native-inputs.hbs +++ b/templates/rust/partials/common/native-inputs.hbs @@ -2,21 +2,21 @@ #[cfg(any(feature = "native", feature = "wasm"))] pub fn populate_inputs(mut payload: TransportMap) -> Result { Ok(Inputs { - {{#each fields }} - {{#ifCond type.name.value "==" "raw"}} - {{snakeCase name.value}}: payload.consume_raw("{{name.value}}")?.into(), + {{#each . }} + {{#ifCond type "==" "raw"}} + {{snakeCase @key}}: payload.consume_raw("{{@key}}")?.into(), {{else}} - {{snakeCase name.value}}: payload.consume("{{name.value}}")?, + {{snakeCase @key}}: payload.consume("{{@key}}")?, {{/ifCond}} {{/each}} }) } -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct Inputs { -{{#each fields }} - #[serde(rename = "{{name.value}}")] - pub {{snakeCase name.value}}: {{> expand-type type}}, +{{#each . }} + #[serde(rename = "{{@key}}")] + pub {{snakeCase @key}}: {{> expand-type .}}, {{/each}} } @@ -24,11 +24,11 @@ pub struct Inputs { impl From for TransportMap { fn from(inputs: Inputs) -> TransportMap { let mut map = TransportMap::new(); - {{#each fields }} - {{#ifCond type.name.value "==" "raw"}} - map.insert("{{snakeCase name.value}}".to_owned(), inputs.{{snakeCase name.value}}.into()); + {{#each . }} + {{#ifCond type "==" "raw"}} + map.insert("{{snakeCase @key}}".to_owned(), inputs.{{snakeCase @key}}.into()); {{else}} - map.insert("{{snakeCase name.value}}".to_owned(), MessageTransport::success(&inputs.{{snakeCase name.value}})); + map.insert("{{snakeCase @key}}".to_owned(), MessageTransport::success(&inputs.{{snakeCase @key}})); {{/ifCond}} {{/each}} @@ -40,8 +40,8 @@ impl From for TransportMap { #[cfg(any(feature = "native", feature = "wasm"))] pub fn inputs_list() -> HashMap { let mut map = HashMap::new(); - {{#each fields}} - map.insert("{{name.value}}".to_owned(),{{> type-signature type}}); + {{#each .}} + map.insert("{{@key}}".to_owned(),{{> type-signature .}}); {{/each}} map } \ No newline at end of file diff --git a/templates/rust/partials/common/native-outputs.hbs b/templates/rust/partials/common/native-outputs.hbs index 71a1b20..4e61e90 100644 --- a/templates/rust/partials/common/native-outputs.hbs +++ b/templates/rust/partials/common/native-outputs.hbs @@ -2,8 +2,8 @@ #[derive(Debug, Default)] #[cfg(feature = "provider")] pub struct OutputPorts { - {{#each fields}} - pub {{snakeCase name.value}}: {{pascalCase name.value}}PortSender, + {{#each .}} + pub {{snakeCase @key}}: {{pascalCase @key}}PortSender, {{/each}} } @@ -11,30 +11,30 @@ pub struct OutputPorts { #[cfg(any(feature = "native", feature = "wasm"))] pub fn outputs_list() -> HashMap { let mut map = HashMap::new(); - {{#each fields}} - map.insert("{{name.value}}".to_owned(),{{> type-signature type}}); + {{#each .}} + map.insert("{{@key}}".to_owned(),{{> type-signature .}}); {{/each}} map } -{{#each fields}} +{{#each .}} #[derive(Debug)] #[cfg(feature = "provider")] -pub struct {{pascalCase name.value}}PortSender { +pub struct {{pascalCase @key}}PortSender { port: PortChannel, } #[cfg(feature = "provider")] -impl Default for {{pascalCase name.value}}PortSender { +impl Default for {{pascalCase @key}}PortSender { fn default() -> Self { Self { - port: PortChannel::new("{{name.value}}"), + port: PortChannel::new("{{@key}}"), } } } #[cfg(feature = "provider")] -impl PortSender for {{pascalCase name.value}}PortSender { +impl PortSender for {{pascalCase @key}}PortSender { fn get_port(&self) -> Result<&PortChannel, ProviderError> { if self.port.is_closed() { Err(ProviderError::SendChannelClosed) @@ -54,8 +54,8 @@ impl PortSender for {{pascalCase name.value}}PortSender { pub fn get_outputs() -> (OutputPorts, TransportStream) { let mut outputs = OutputPorts::default(); let mut ports = vec![ - {{#each fields}} - &mut outputs.{{snakeCase name.value}}.port, + {{#each .}} + &mut outputs.{{snakeCase @key}}.port, {{/each}} ]; let stream = PortChannel::merge_all(&mut ports); @@ -71,10 +71,10 @@ pub struct Outputs { #[cfg(all(feature = "native", feature = "guest"))] impl Outputs { - {{#each fields}} - pub async fn {{snakeCase name.value}}(&mut self)-> Result expand-type type }}>, ProviderError> { - let packets = self.packets.take("{{name.value}}").await; - Ok(PortOutput::new("{{name.value}}".to_owned(), packets)) + {{#each .}} + pub async fn {{snakeCase @key}}(&mut self)-> Result expand-type . }}>, ProviderError> { + let packets = self.packets.take("{{@key}}").await; + Ok(PortOutput::new("{{@key}}".to_owned(), packets)) } {{/each}} } @@ -82,9 +82,9 @@ impl Outputs { #[cfg(all(feature = "wasm", feature = "guest"))] impl Outputs { {{#each fields}} - pub fn {{snakeCase name.value}}(&mut self)-> Result { - let packets = self.packets.take("{{name.value}}").ok_or_else(|| WasmError::ResponseMissing("{{name.value}}".to_owned()))?; - Ok(PortOutput::new("{{name.value}}".to_owned(), packets)) + pub fn {{snakeCase @key}}(&mut self)-> Result { + let packets = self.packets.take("{{@key}}").ok_or_else(|| WasmError::ResponseMissing("{{@key}}".to_owned()))?; + Ok(PortOutput::new("{{@key}}".to_owned(), packets)) } {{/each}} } diff --git a/templates/rust/partials/common/native-type-definition.hbs b/templates/rust/partials/common/native-type-definition.hbs deleted file mode 100644 index 09c88f8..0000000 --- a/templates/rust/partials/common/native-type-definition.hbs +++ /dev/null @@ -1,20 +0,0 @@ - - -{{#switch name.value}} - {{#case 'Inputs'}} - {{> native-inputs . }} - {{/case}} - {{#case 'Outputs'}} - {{> native-outputs . }} - {{/case}} - {{#default}} - #[derive(Debug, PartialEq, Deserialize, Serialize, Default, Clone)] - pub struct {{pascalCase name.value}} { - {{#each fields }} - #[serde(rename = "{{name.value}}")] - pub {{snakeCase name.value}}: {{> expand-type type }}, - {{/each}} - } - {{/default}} -{{/switch}} - diff --git a/templates/rust/partials/common/provider-signature.hbs b/templates/rust/partials/common/provider-signature.hbs new file mode 100644 index 0000000..1dfc49d --- /dev/null +++ b/templates/rust/partials/common/provider-signature.hbs @@ -0,0 +1,16 @@ +ProviderSignature { + name: {{#if name}}Some("{{name}}".to_owned()){{else}}None{{/if}}, + types: std::collections::HashMap::from([ + {{#each .}} + ("{{@key}}".to_owned(), StructSignature{ + name:"{{name}}".to_owned(), + fields: std::collections::HashMap::from([ + {{#each fields}} + ("{{@key}}".to_owned(),{{> type-signature type}}), + {{/each}} + ]).into() + }), + {{/each}} + ]).into(), + components: components.into() +} diff --git a/templates/rust/partials/common/struct-definition.hbs b/templates/rust/partials/common/struct-definition.hbs new file mode 100644 index 0000000..f6f2073 --- /dev/null +++ b/templates/rust/partials/common/struct-definition.hbs @@ -0,0 +1,7 @@ +#[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize, Clone)] +pub struct {{pascalCase name}} { + {{#each fields }} + #[serde(rename = "{{@key}}")] + pub {{snakeCase @key}}: {{> expand-type . }}, + {{/each}} +} diff --git a/templates/rust/partials/common/type-signature.hbs b/templates/rust/partials/common/type-signature.hbs index 09c9192..d126350 100644 --- a/templates/rust/partials/common/type-signature.hbs +++ b/templates/rust/partials/common/type-signature.hbs @@ -1,45 +1,44 @@ -{{#switch kind ~}} - {{#case "Named"}} - {{#switch name.value}} - {{#case "i8"}}TypeSignature::I8{{/case}} - {{#case "u8"}}TypeSignature::U8{{/case}} - {{#case "i16"}}TypeSignature::I16{{/case}} - {{#case "u16"}}TypeSignature::U16{{/case}} - {{#case "i32"}}TypeSignature::I32{{/case}} - {{#case "u32"}}TypeSignature::U32{{/case}} - {{#case "i64"}}TypeSignature::I64{{/case}} - {{#case "u64"}}TypeSignature::U64{{/case}} - {{#case "f32"}}TypeSignature::F32{{/case}} - {{#case "f64"}}TypeSignature::F64{{/case}} - {{#case "bool"}}TypeSignature::Bool{{/case}} - {{#case "string"}}TypeSignature::String{{/case}} - {{#case "datetime"}}TypeSignature::Datetime{{/case}} - {{#case "bytes"}}TypeSignature::Bytes{{/case}} - {{#case "raw"}}TypeSignature::Raw{{/case}} - {{#case "value"}}TypeSignature::Value{{/case}} +{{#switch type}} + {{#case "i8"}}TypeSignature::I8{{/case}} + {{#case "u8"}}TypeSignature::U8{{/case}} + {{#case "i16"}}TypeSignature::I16{{/case}} + {{#case "u16"}}TypeSignature::U16{{/case}} + {{#case "i32"}}TypeSignature::I32{{/case}} + {{#case "u32"}}TypeSignature::U32{{/case}} + {{#case "i64"}}TypeSignature::I64{{/case}} + {{#case "u64"}}TypeSignature::U64{{/case}} + {{#case "f32"}}TypeSignature::F32{{/case}} + {{#case "f64"}}TypeSignature::F64{{/case}} + {{#case "bool"}}TypeSignature::Bool{{/case}} + {{#case "string"}}TypeSignature::String{{/case}} + {{#case "datetime"}}TypeSignature::Datetime{{/case}} + {{#case "bytes"}}TypeSignature::Bytes{{/case}} + {{#case "raw"}}TypeSignature::Raw{{/case}} + {{#case "value"}}TypeSignature::Value{{/case}} - {{#case "link"}}Link{provider:None}{{/case}} + {{#case "link"}}Link{provider:Some("{{provider}}".to_owned())}{{/case}} - {{#default}}{{name.value}}{{/default}} - {{/switch}} + {{#case "map"}} + TypeSignature::Map{ + key:Box::new({{> type-signature key}}) + value:Box::new({{> type-signature value }}) + } {{/case}} - {{#case "MapType"}} - TypeSignature::Map{ - key:{{> type-signature keyType}}.into(), - value:{{> type-signature valueType }}.into() - } - {{/case}} - {{#case "ListType"}} - TypeSignature::List{ - element:{{> type-signature type}}.into(), - } + + {{#case "list"}} + TypeSignature::List{ + element:Box::new({{> type-signature element}}), + } {{/case}} - {{#case "Optional"}} - TypeSignature::Optional{ - option:{{> type-signature type}}.into(), - } + + {{#case "optional"}} + TypeSignature::Optional{ + option:Box::new({{> type-signature type}}), + } {{/case}} + {{#default}} - unknown + {{debug .}} + {{panic "Unknown type..."}} {{/default}} -{{~/switch}} +{{/switch}} diff --git a/templates/rust/partials/common/types.hbs b/templates/rust/partials/common/types.hbs new file mode 100644 index 0000000..916d033 --- /dev/null +++ b/templates/rust/partials/common/types.hbs @@ -0,0 +1,12 @@ +{{#ifEmpty types}} +pub mod types { + // no additional types +} +{{else}} +pub mod types { + use vino_provider::prelude::*; + {{#each types}} + {{> struct-definition . }} + {{/each}} +} +{{/ifEmpty}} diff --git a/templates/rust/partials/wapc-integration/Inputs.hbs b/templates/rust/partials/wapc-integration/Inputs.hbs deleted file mode 100644 index 89c89cd..0000000 --- a/templates/rust/partials/wapc-integration/Inputs.hbs +++ /dev/null @@ -1,35 +0,0 @@ - - -fn populate_inputs(payload: &IncomingPayload) -> Result { - Ok(Inputs { - {{#each fields }} - {{snakeCase name.value}}: deserialize(payload.get("{{name.value}}")?)?, - {{/each}} - }) -} - -impl From for TransportMap { - fn from(inputs: Inputs) -> TransportMap { - let mut map = TransportMap::new(); - {{#each fields }} - {{#switch type.name.value}} - {{#case "raw"}} - map.insert("{{snakeCase name.value}}".to_owned(), inputs.{{snakeCase name.value}}.into()); - {{/case}} - {{#default}} - map.insert("{{snakeCase name.value}}".to_owned(), MessageTransport::success(&inputs.{{snakeCase name.value}})); - {{/default}} - {{/switch}} - {{/each}} - map - } -} - -#[derive(Debug,Deserialize, Serialize, Clone)] -pub struct Inputs { -{{#each fields }} - #[serde(rename = "{{name.value}}")] - pub {{snakeCase name.value}}: {{> expand-type type}}, -{{/each}} -} - diff --git a/templates/rust/partials/wapc-integration/Outputs.hbs b/templates/rust/partials/wapc-integration/Outputs.hbs deleted file mode 100644 index 8d4847d..0000000 --- a/templates/rust/partials/wapc-integration/Outputs.hbs +++ /dev/null @@ -1,50 +0,0 @@ - -#[derive(Debug)] -pub struct OutputPorts { -{{#each fields}} - pub {{snakeCase name.value}}: {{pascalCase name.value}}Sender, -{{/each}} -} - -{{#each fields}} -#[derive(Debug, PartialEq, Clone)] -pub struct {{pascalCase name.value}}Sender { id: u32 } - -impl PortSender for {{pascalCase name.value}}Sender { - type PayloadType = {{> expand-type type}}; - fn get_name(&self) -> String { - "{{name.value}}".to_string() - } - fn get_id(&self) -> u32 { - self.id - } -} -{{/each}} - -fn get_outputs(id:u32) -> OutputPorts { - OutputPorts { - {{#each fields}} - {{snakeCase name.value}}: {{pascalCase name.value}}Sender { id }, - {{/each}} - } -} - -#[derive(Debug)] -pub struct Outputs { - packets: ProviderOutput -} - -impl Outputs { - {{#each fields}} - pub fn {{snakeCase name.value}}(&mut self)-> Result { - let packets = self.packets.take("{{name.value}}").ok_or_else(||ComponentError::new("No packets for port '{{name.value}}' found"))?; - Ok(PortOutput::new("{{name.value}}".to_owned(), packets)) - } - {{/each}} -} - -impl From for Outputs { - fn from(packets: ProviderOutput) -> Self { - Self{packets} - } -} \ No newline at end of file diff --git a/templates/rust/partials/wapc-integration/TypeDefinition.hbs b/templates/rust/partials/wapc-integration/TypeDefinition.hbs deleted file mode 100644 index 5737e63..0000000 --- a/templates/rust/partials/wapc-integration/TypeDefinition.hbs +++ /dev/null @@ -1,19 +0,0 @@ - - -{{#switch name.value}} - {{#case 'Inputs'}} - {{> Inputs . }} - {{/case}} - {{#case 'Outputs'}} - {{> Outputs . }} - {{/case}} - {{#default}} - #[derive(Debug, PartialEq, Deserialize, Serialize, Default, Clone)] - pub struct {{pascalCase name.value}} { - {{#each fields }} - #[serde(rename = "{{name.value}}")] - pub {{snakeCase name.value}}: {{> expand-type type }}, - {{/each}} - } - {{/default}} -{{/switch}} diff --git a/templates/rust/partials/wapc-integration/wasm-inputs.hbs b/templates/rust/partials/wapc-integration/wasm-inputs.hbs new file mode 100644 index 0000000..fbf4f54 --- /dev/null +++ b/templates/rust/partials/wapc-integration/wasm-inputs.hbs @@ -0,0 +1,35 @@ + + +fn populate_inputs(payload: &IncomingPayload) -> Result { + Ok(Inputs { + {{#each . }} + {{snakeCase @key}}: deserialize(payload.get("{{@key}}")?)?, + {{/each}} + }) +} + +impl From for TransportMap { + fn from(inputs: Inputs) -> TransportMap { + let mut map = TransportMap::new(); + {{#each . }} + {{#switch type}} + {{#case "raw"}} + map.insert("{{snakeCase @key}}".to_owned(), inputs.{{snakeCase @key}}.into()); + {{/case}} + {{#default}} + map.insert("{{snakeCase @key}}".to_owned(), MessageTransport::success(&inputs.{{snakeCase @key}})); + {{/default}} + {{/switch}} + {{/each}} + map + } +} + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct Inputs { +{{#each . }} + #[serde(rename = "{{@key}}")] + pub {{snakeCase @key}}: {{> expand-type .}}, +{{/each}} +} + diff --git a/templates/rust/partials/wapc-integration/wasm-outputs.hbs b/templates/rust/partials/wapc-integration/wasm-outputs.hbs new file mode 100644 index 0000000..f7aef2d --- /dev/null +++ b/templates/rust/partials/wapc-integration/wasm-outputs.hbs @@ -0,0 +1,50 @@ + +#[derive(Debug)] +pub struct OutputPorts { +{{#each .}} + pub {{snakeCase @key}}: {{pascalCase @key}}Sender, +{{/each}} +} + +{{#each .}} +#[derive(Debug, PartialEq, Clone)] +pub struct {{pascalCase @key}}Sender { id: u32 } + +impl PortSender for {{pascalCase @key}}Sender { + type PayloadType = {{> expand-type .}}; + fn get_name(&self) -> String { + "{{@key}}".to_string() + } + fn get_id(&self) -> u32 { + self.id + } +} +{{/each}} + +fn get_outputs(id:u32) -> OutputPorts { + OutputPorts { + {{#each .}} + {{snakeCase @key}}: {{pascalCase @key}}Sender { id }, + {{/each}} + } +} + +#[derive(Debug)] +pub struct Outputs { + packets: ProviderOutput +} + +impl Outputs { + {{#each .}} + pub fn {{snakeCase @key}}(&mut self)-> Result { + let packets = self.packets.take("{{@key}}").ok_or_else(||ComponentError::new("No packets for port '{{@key}}' found"))?; + Ok(PortOutput::new("{{@key}}".to_owned(), packets)) + } + {{/each}} +} + +impl From for Outputs { + fn from(packets: ProviderOutput) -> Self { + Self{packets} + } +} \ No newline at end of file diff --git a/templates/rust/partials/wellknown-integration/component.hbs b/templates/rust/partials/wellknown-implementer/component.hbs similarity index 94% rename from templates/rust/partials/wellknown-integration/component.hbs rename to templates/rust/partials/wellknown-implementer/component.hbs index 208b82c..e4de889 100644 --- a/templates/rust/partials/wellknown-integration/component.hbs +++ b/templates/rust/partials/wellknown-implementer/component.hbs @@ -2,9 +2,7 @@ use std::collections::HashMap; use async_trait::async_trait; -use vino_provider::native::prelude::*; - - +use vino_provider::prelude::*; #[derive(Default)] pub(crate) struct Component {} diff --git a/templates/rust/provider-component-module.hbs b/templates/rust/provider-component-module.hbs deleted file mode 100644 index 13eeb07..0000000 --- a/templates/rust/provider-component-module.hbs +++ /dev/null @@ -1,7 +0,0 @@ -/********************************************** -***** This file is generated, do not edit ***** -***********************************************/ - -{{#each schemas}} -pub(crate) mod {{snakeCase unhyphenated}}; -{{/each}} diff --git a/templates/rust/provider-component.hbs b/templates/rust/provider-component.hbs index b690a3d..c4d17d5 100644 --- a/templates/rust/provider-component.hbs +++ b/templates/rust/provider-component.hbs @@ -1,5 +1,5 @@ -use crate::generated::{{snakeCase schema.unhyphenated}}::*; +pub use crate::components::generated::{{snakeCase schema.unhyphenated}}::*; pub(crate) async fn job( input: Inputs, diff --git a/templates/rust/provider-integration.hbs b/templates/rust/provider-integration.hbs index 37cf084..360e1e2 100644 --- a/templates/rust/provider-integration.hbs +++ b/templates/rust/provider-integration.hbs @@ -2,9 +2,9 @@ ***** This file is generated, do not edit ***** ***********************************************/ -use vino_provider::native::prelude::*; +pub use vino_provider::prelude::*; -use crate::generated; +{{> component-imports . }} #[derive(Debug)] pub(crate) struct Dispatcher {} @@ -16,10 +16,9 @@ impl Dispatch for Dispatcher { context: Self::Context, data: TransportMap, ) -> Result>{ - use generated::*; let result = match op { - {{#each schemas}} - "{{document.namespace.name.value}}" => {{snakeCase file.unhyphenated}}::Component::default().execute(context, data).await, + {{#each interface.components}} + "{{name}}" => generated::{{snakeCase name}}::Component::default().execute(context, data).await, {{/each}} _ => Err(Box::new(NativeComponentError::new(format!( "Component not found on this provider: {}", @@ -30,89 +29,67 @@ impl Dispatch for Dispatcher { } } -pub(crate) fn get_signature() -> ProviderSignature { - use std::collections::HashMap; - let mut components = HashMap::new(); +pub fn get_signature() -> ProviderSignature { + let mut components = std::collections::HashMap::new(); - {{#each schemas}} - components.insert("{{document.namespace.name.value}}".to_owned(), generated::{{snakeCase file.unhyphenated}}::signature()); + {{#each interface.components}} + components.insert("{{name}}".to_owned(), generated::{{snakeCase name}}::signature()); {{/each}} - ProviderSignature { - name: "".to_owned(), - types: StructMap::todo(), - components: components.into() - } + {{> provider-signature interface}} } -{{#each schemas}} -pub (crate) mod {{snakeCase file.unhyphenated}} { - #![allow(unused,unreachable_pub)] - use std::collections::HashMap; +{{> types interface }} - use async_trait::async_trait; - use serde::{ - Deserialize, - Serialize, - }; +pub mod generated { +{{#each interface.components}} + pub mod {{snakeCase name}} { + #![allow(unused,unreachable_pub)] + use std::collections::HashMap; - #[cfg(feature = "native")] - pub use vino_provider::native::prelude::*; + use async_trait::async_trait; - #[cfg(feature = "wasm")] - pub use vino_provider::wasm::prelude::*; + pub use vino_provider::prelude::*; - pub(crate) fn signature() -> ComponentSignature { - ComponentSignature { - name: "{{document.namespace.name.value }}".to_owned(), - inputs : inputs_list().into(), - outputs : outputs_list().into(), + pub fn signature() -> ComponentSignature { + ComponentSignature { + name: "{{name}}".to_owned(), + inputs : inputs_list().into(), + outputs : outputs_list().into(), + } } - } - {{#with document}} - #[derive(Default)] - pub(crate) struct Component {} + #[derive(Default)] + pub struct Component {} - #[async_trait] - impl NativeComponent for Component { - type Context = crate::Context; - async fn execute( - &self, - context: Self::Context, - data: TransportMap, - ) -> Result> { - let inputs = populate_inputs(data).map_err(|e| NativeComponentError::new(e.to_string()))?; - let (outputs, stream) = get_outputs(); - let result = tokio::spawn(crate::components::{{snakeCase namespace.name.value}}::job(inputs, outputs, context)) - .await - .map_err(|e| { - Box::new(NativeComponentError::new(format!( - "Component error: {}", - e - ))) - })?; - match result { - Ok(_) => Ok(stream), - Err(e) => Err(Box::new(NativeComponentError::new(e.to_string()))), + #[async_trait] + impl NativeComponent for Component { + type Context = crate::Context; + async fn execute( + &self, + context: Self::Context, + data: TransportMap, + ) -> Result> { + let inputs = populate_inputs(data).map_err(|e| NativeComponentError::new(e.to_string()))?; + let (outputs, stream) = get_outputs(); + let result = tokio::spawn(crate::components::{{snakeCase name}}::job(inputs, outputs, context)) + .await + .map_err(|e| { + Box::new(NativeComponentError::new(format!( + "Component error: {}", + e + ))) + })?; + match result { + Ok(_) => Ok(stream), + Err(e) => Err(Box::new(NativeComponentError::new(e.to_string()))), + } } } - } - {{#each definitions}} - {{#switch kind}} - {{#case "NamespaceDefinition"}}{{/case}} - {{#case "TypeDefinition"}} - {{#switch name.value}} - {{> native-type-definition .}} - {{/switch}} - {{/case}} - {{#default}} - {{debug .}} - {{panic "WIDL Node not yet handled"}} - {{/default}} - {{/switch}} - {{/each}} - {{/with}} + {{> native-inputs inputs }} + {{> native-outputs outputs }} + + } +{{/each}} } -{{/each}} \ No newline at end of file diff --git a/templates/rust/wapc-component-module.hbs b/templates/rust/wapc-component-module.hbs deleted file mode 100644 index 13eeb07..0000000 --- a/templates/rust/wapc-component-module.hbs +++ /dev/null @@ -1,7 +0,0 @@ -/********************************************** -***** This file is generated, do not edit ***** -***********************************************/ - -{{#each schemas}} -pub(crate) mod {{snakeCase unhyphenated}}; -{{/each}} diff --git a/templates/rust/wapc-component.hbs b/templates/rust/wapc-component.hbs index f43776e..eb1844c 100644 --- a/templates/rust/wapc-component.hbs +++ b/templates/rust/wapc-component.hbs @@ -1,6 +1,6 @@ -use crate::generated::{{snakeCase schema.unhyphenated}}::*; +pub use crate::components::generated::{{snakeCase schema.unhyphenated}}::*; pub(crate) fn job(input: Inputs, output: OutputPorts) -> JobResult { Ok(()) diff --git a/templates/rust/wapc-integration.hbs b/templates/rust/wapc-integration.hbs index a02b4fc..a1fc14e 100644 --- a/templates/rust/wapc-integration.hbs +++ b/templates/rust/wapc-integration.hbs @@ -2,7 +2,9 @@ ***** This file is generated, do not edit ***** ***********************************************/ -use vino_provider::wasm::prelude::*; +pub use vino_provider::prelude::*; + +{{> component-imports . }} type Result = std::result::Result; @@ -42,9 +44,7 @@ pub(crate) extern "C" fn __guest_call(op_len: i32, req_len: i32) -> i32 { } static ALL_COMPONENTS: &[&str] = &[ -{{#each schemas}} -"{{document.namespace.name.value}}", -{{/each}} +{{#each interface.components}}"{{name}}",{{/each}} ]; pub struct Dispatcher {} @@ -52,8 +52,8 @@ impl Dispatch for Dispatcher { fn dispatch(op: &str, payload: &[u8]) -> CallResult { let payload = IncomingPayload::from_buffer(payload)?; let result = match op { - {{#each schemas}} - "{{document.namespace.name.value}}" => {{snakeCase file.unhyphenated}}::Component::new().execute(&payload), + {{#each interface.components}} + "{{name}}" => {{snakeCase name}}::Component::default().execute(&payload), {{/each}} _ => Err(WasmError::ComponentNotFound(op.to_owned(), ALL_COMPONENTS.join(", "))), }?; @@ -61,48 +61,32 @@ impl Dispatch for Dispatcher { } } -{{#each schemas}} -pub mod {{snakeCase file.unhyphenated}} { - use crate::components::{{snakeCase file.unhyphenated}} as implementation; - - use serde::{ - Deserialize, - Serialize, - }; - pub use vino_provider::wasm::prelude::*; +{{> types interface }} +pub mod generated { use super::*; +{{#each interface.components}} + pub mod {{snakeCase name}} { + use crate::components::{{snakeCase name}} as implementation; - pub struct Component {} + pub use vino_provider::prelude::*; - impl Component { - pub fn new() -> Self { - Self { } - } - } - impl WapcComponent for Component { - fn execute(&self, payload: &IncomingPayload) -> JobResult { - let outputs = get_outputs(payload.id()); - let inputs = populate_inputs(payload)?; - implementation::job(inputs, outputs) + use super::*; + + #[derive(Default)] + pub struct Component {} + + impl WapcComponent for Component { + fn execute(&self, payload: &IncomingPayload) -> JobResult { + let outputs = get_outputs(payload.id()); + let inputs = populate_inputs(payload)?; + implementation::job(inputs, outputs) + } } - } -{{#with document}} -{{#each definitions}} -{{#switch kind}} - {{#case "NamespaceDefinition"}}{{/case}} - {{#case "TypeDefinition"}} - {{#switch name.value}} - {{> TypeDefinition .}} - {{/switch}} - {{/case}} - {{#default}} - {{debug .}} - {{panic "WIDL Node not yet handled"}} - {{/default}} -{{/switch}} + {{> wasm-inputs inputs }} + {{> wasm-outputs outputs }} + + } {{/each}} -{{/with}} } -{{/each}} diff --git a/templates/rust/wapc-lib.hbs b/templates/rust/wapc-lib.hbs index 98cd248..a6b489c 100644 --- a/templates/rust/wapc-lib.hbs +++ b/templates/rust/wapc-lib.hbs @@ -1,3 +1,2 @@ mod generated; pub mod components; - diff --git a/templates/rust/wellknown-component-module.hbs b/templates/rust/wellknown-component-module.hbs deleted file mode 100644 index 6728a10..0000000 --- a/templates/rust/wellknown-component-module.hbs +++ /dev/null @@ -1,7 +0,0 @@ -/********************************************** -***** This file is generated, do not edit ***** -***********************************************/ - -{{#each interface.components}} -pub(crate) mod {{snakeCase name}}; -{{/each}} diff --git a/templates/rust/wellknown-integration.hbs b/templates/rust/wellknown-implementer.hbs similarity index 62% rename from templates/rust/wellknown-integration.hbs rename to templates/rust/wellknown-implementer.hbs index 332556b..216c7ae 100644 --- a/templates/rust/wellknown-integration.hbs +++ b/templates/rust/wellknown-implementer.hbs @@ -2,7 +2,9 @@ ***** This file is generated, do not edit ***** ***********************************************/ -pub(crate) use vino_provider::native::prelude::*; +pub(crate) use vino_provider::prelude::*; + +{{> component-imports .}} pub(crate) fn get_signature() -> ProviderSignature { use std::collections::HashMap; @@ -10,16 +12,8 @@ pub(crate) fn get_signature() -> ProviderSignature { {{#each interface.components}} components.insert("{{name}}".to_owned(), {{snakeCase @root.interface.name}}::{{snakeCase name}}::signature()); {{/each}} - {{#each schemas}} - components.insert("{{document.namespace.name.value}}".to_owned(), generated::{{snakeCase file.unhyphenated}}::signature()); - {{/each}} - - ProviderSignature { - name: "".to_owned(), - types: StructMap::todo(), - components: components.into() - } + {{> provider-signature interface}} } @@ -35,7 +29,7 @@ impl Dispatch for Dispatcher { ) -> Result>{ let result = match op { {{#each interface.components}} - "{{ name }}" => self::{{snakeCase name}}::Component::default().execute(context, data).await, + "{{ name }}" => self::generated::{{snakeCase name}}::Component::default().execute(context, data).await, {{/each}} _ => Err(Box::new(NativeComponentError::new(format!( "Component not found on this provider: {}", @@ -46,12 +40,13 @@ impl Dispatch for Dispatcher { } } -{{#each interface.components}} -pub (crate) mod {{snakeCase name}} { - #![allow(unused)] - use {{snakeCase @root.interface.name}}::{{snakeCase name}}::*; - +pub mod generated { + {{#each interface.components}} + pub (crate) mod {{snakeCase name}} { + #![allow(unused)] + use {{snakeCase @root.interface.name}}::{{snakeCase name}}::*; - {{> component . }} + {{> component . }} + } + {{/each}} } -{{/each}} \ No newline at end of file diff --git a/test/fixtures/hello-world.widl b/test/fixtures/hello-world.widl index 53b9942..dbc88cc 100644 --- a/test/fixtures/hello-world.widl +++ b/test/fixtures/hello-world.widl @@ -3,7 +3,7 @@ namespace "hello-world" "Example inputs" type Inputs { - message: string + messages: [string] } "Example outputs" diff --git a/test/fixtures/rust-project/.cargo/config.toml b/test/fixtures/rust-project/.cargo/config.toml new file mode 100644 index 0000000..0723fed --- /dev/null +++ b/test/fixtures/rust-project/.cargo/config.toml @@ -0,0 +1,7 @@ +[build] +target = "wasm32-unknown-unknown" + +[profile.release] +# Optimize for small code size +opt-level = "z" +lto = true diff --git a/test/fixtures/rust-project/.github/workflows/build.yml b/test/fixtures/rust-project/.github/workflows/build.yml new file mode 100644 index 0000000..4b10233 --- /dev/null +++ b/test/fixtures/rust-project/.github/workflows/build.yml @@ -0,0 +1,23 @@ +name: build + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Install Rust bits + run: rustup target add wasm32-unknown-unknown + - name: Build + run: cargo build --verbose --target wasm32-unknown-unknown --release + - name: Run tests + run: cargo test --verbose diff --git a/test/fixtures/rust-project/.gitignore b/test/fixtures/rust-project/.gitignore new file mode 100644 index 0000000..848b2ca --- /dev/null +++ b/test/fixtures/rust-project/.gitignore @@ -0,0 +1,15 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + + +#Added by cargo + +/target diff --git a/test/fixtures/rust-project/.vscode/extensions.json b/test/fixtures/rust-project/.vscode/extensions.json new file mode 100644 index 0000000..21c1217 --- /dev/null +++ b/test/fixtures/rust-project/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["matklad.rust-analyzer", "vadimcn.vscode-lldb"] +} diff --git a/test/fixtures/rust-project/.vscode/launch.json b/test/fixtures/rust-project/.vscode/launch.json new file mode 100644 index 0000000..3d81b17 --- /dev/null +++ b/test/fixtures/rust-project/.vscode/launch.json @@ -0,0 +1,28 @@ +{ + "configurations": [ + { + "name": "Debug Native", + "type": "lldb", + "request": "launch", + "program": "${workspaceFolder}/target/debug/${workspaceRootFolderName}", + "args": [], + "cwd": "${workspaceFolder}", + "sourceLanguages": ["rust"], + "preLaunchTask": "Cargo: Build Native" + }, + { + "name": "Debug WASM", + "type": "lldb", + "request": "launch", + "program": "wasmtime", + "args": [ + "run", + "${workspaceFolder}/target/wasm32-unknown-unknown/debug/${workspaceRootFolderName}.wasm", + "-g" + ], + "cwd": "${workspaceFolder}", + "sourceLanguages": ["rust"], + "preLaunchTask": "Cargo: Build WASM" + } + ] +} diff --git a/test/fixtures/rust-project/.vscode/settings.json b/test/fixtures/rust-project/.vscode/settings.json new file mode 100644 index 0000000..1b5f6eb --- /dev/null +++ b/test/fixtures/rust-project/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.watcherExclude": { + "**/.git/objects/**": true, + "**/.git/subtree-cache/**": true, + "**/node_modules/**": true, + "**/.hg/store/**": true, + "**/target/**": true + } +} diff --git a/test/fixtures/rust-project/.vscode/tasks.json b/test/fixtures/rust-project/.vscode/tasks.json new file mode 100644 index 0000000..05ed5f9 --- /dev/null +++ b/test/fixtures/rust-project/.vscode/tasks.json @@ -0,0 +1,29 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Cargo: Build Native", + "type": "shell", + "command": "cargo", + "args": [ + "build" + ], + "problemMatcher": [ + "$rustc" + ] + }, + { + "label": "Cargo: Build WASM", + "type": "shell", + "command": "cargo", + "args": [ + "build", + "--target", + "wasm32-unknown-unknown" + ], + "problemMatcher": [ + "$rustc" + ] + } + ] +} diff --git a/test/fixtures/rust-project/Cargo.toml b/test/fixtures/rust-project/Cargo.toml new file mode 100644 index 0000000..bf0d487 --- /dev/null +++ b/test/fixtures/rust-project/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "rust-project" +version = "0.0.0" +description = "" +authors = ["Jarrod Overson"] +edition = "2018" +license = "BSD-3-Clause" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib"] + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +vino-provider = { path = "../../../../vino/sdk/crates/vino-provider/", features = [ + "wasm", +] } + +[dev-dependencies] diff --git a/test/fixtures/rust-project/LICENSE b/test/fixtures/rust-project/LICENSE new file mode 100644 index 0000000..a6b65d2 --- /dev/null +++ b/test/fixtures/rust-project/LICENSE @@ -0,0 +1,29 @@ +BSD-3 License + +Copyright (c) Jarrod Overson +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/test/fixtures/rust-project/Makefile b/test/fixtures/rust-project/Makefile new file mode 100644 index 0000000..24c22d5 --- /dev/null +++ b/test/fixtures/rust-project/Makefile @@ -0,0 +1,123 @@ +.DEFAULT_GOAL:=all + +.PHONY: all codegen build clean doc test help list + +ifdef OS +_OS := $(OS) +else +_OS := "" +endif + +ifeq ($(_OS),Windows_NT) +# Use powershell on Windows +SHELL := powershell.exe +# mkdir -p on windows throws an error if the directory exists, -Force suppresses it. +MKDIR := mkdir -Force -p +.SHELLFLAGS := -NoProfile -Command +else +# Enforce bash as the shell for consistency +SHELL := bash +MKDIR := mkdir -p +# Use bash strict mode +.SHELLFLAGS := -eu -o pipefail -c +endif + +MAKEFLAGS += --warn-undefined-variables +MAKEFLAGS += --no-builtin-rules + +SCHEMA_DIR := ./schemas +COMPONENTS_DIR := ./src/components +GENERATED_MODULE := ./src/components.rs + +# Get list of WIDL files in SCHEMA_DIR +SCHEMAS=$(wildcard ${SCHEMA_DIR}/*.widl) + +# Translate a list of WIDL files to COMPONENTS_DIR/file.rs (transposing hyphens to underscores) +GENERATED_COMPONENTS := $(patsubst %.widl,%.rs,$(patsubst ${SCHEMA_DIR}%,${COMPONENTS_DIR}%,$(subst -,_,$(SCHEMAS)))) + +# Name of the package from Cargo.toml +CRATE_NAME:=$(shell tomlq -f Cargo.toml package.name) +# Name with hyphens substituted with underscores +CRATE_FS_NAME:=$(subst -,_,$(CRATE_NAME)) +# Version from Cargo.toml +CRATE_VERSION:=$(shell tomlq -f Cargo.toml package.version) +# Get the root directory +WORKSPACE_ROOT:=. +# Directory to store the build artifacts +ARTIFACT_DIR:=build +# Path to copy the build artifact to +BUILD_ARTIFACT:=$(ARTIFACT_DIR)/$(CRATE_FS_NAME).wasm +# Path for the signed artifact +SIGNED_ARTIFACT:=$(ARTIFACT_DIR)/$(CRATE_FS_NAME)_s.wasm +# Path to the interface JSON +INTERFACE:=./interface.json + +VINOC=cargo run --manifest-path ../../../Cargo.toml -p vinoc +CODEGEN:=npm exec --package ../../../ vino-codegen -- + +# Files to clean on make clean +CLEAN_FILES := $(GENERATED_MODULE) $(BUILD_ARTIFACT) $(SIGNED_ARTIFACT) ./src/components/mod.rs $(INTERFACE) + +$(SIGNED_ARTIFACT): $(BUILD_ARTIFACT) $(INTERFACE) + @echo Signing $(BUILD_ARTIFACT) + @$(VINOC) -- sign $(BUILD_ARTIFACT) $(INTERFACE) --ver=$(CRATE_VERSION) --rev=0 + @echo Created $(SIGNED_ARTIFACT) + +# Defines rules like the following for each schema found : +# src/components/my_components.rs: schemas/my-component.widl +define WIDL_CODEGEN +$(patsubst %.widl,%.rs,$(patsubst ${SCHEMA_DIR}%,${COMPONENTS_DIR}%,$(subst -,_,$(1)))): $1 + @$$(shell $$(MKDIR) $$(dir $$@)) + @echo Generating $$@ + @$(CODEGEN) rust wapc-component $(notdir $(basename $(subst -,_,$(1)))) -o $$@ + @rustfmt $$@ +endef + +# Call the above rule generator for each schema file +$(foreach schema,$(SCHEMAS),$(eval $(call WIDL_CODEGEN,$(schema)))) + +$(GENERATED_MODULE): $(SCHEMAS) + @echo Generating $@ + @$(CODEGEN) rust wapc-integration $(SCHEMA_DIR) -f -o $@ + @rustfmt $@ + +$(COMPONENTS_DIR): + @echo Making directory \"$@\" + $(shell $(MKDIR) $@) + +$(ARTIFACT_DIR): + @echo Making directory \"$@\" + $(shell $(MKDIR) $@) + +$(BUILD_ARTIFACT): $(wildcard src/*.rs) ./src/lib.rs $(ARTIFACT_DIR) $(GENERATED_MODULE) $(GENERATED_COMPONENTS) + @echo Building artifact + cargo build --target wasm32-unknown-unknown --release + @echo Copying binary to $(BUILD_ARTIFACT) + @cp $(WORKSPACE_ROOT)/target/wasm32-unknown-unknown/release/$(CRATE_FS_NAME).wasm build/ + +##@ Targets + +$(INTERFACE): $(SCHEMAS) ## Create an interface.json from the project's schemas + @echo Building $@ from schemas in $(SCHEMA_DIR) + @$(CODEGEN) json interface "$(CRATE_NAME)" $(SCHEMA_DIR) -o $@ -f + +all: $(SIGNED_ARTIFACT) ## Make and sign the wasm binary + +clean: ## Clean the generated files + @rm -f $(CLEAN_FILES) + +codegen: $(INTERFACE) $(GENERATED_COMPONENTS) $(GENERATED_MODULE) ./src/lib.rs ## Generate code from schemas + +doc: ## Generate documentation + @echo Unimplemented + +test: build ## Run tests + cargo test + +##@ Helpers + +list: ## Print schemas + @echo $(SCHEMAS) + +help: ## Display this help + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z0-9_\-.*]+:.*?##/ { printf " \033[36m%-32s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) diff --git a/test/fixtures/rust-project/README.md b/test/fixtures/rust-project/README.md new file mode 100644 index 0000000..e0f1a28 --- /dev/null +++ b/test/fixtures/rust-project/README.md @@ -0,0 +1,24 @@ +# rust-project + +## Building + +The Makefile includes the recipes necessary to generate code from the WIDL schemas in ./schemas/. + +To codegen, build, and sign your WASM module, run `make` + +```shell +$ make +``` + +## Codegen and clean + +```shell +$ make clean && make codegen +``` + +## Testing + +```shell +$ make test +``` + diff --git a/test/fixtures/rust-project/interface.json b/test/fixtures/rust-project/interface.json new file mode 100644 index 0000000..036c643 --- /dev/null +++ b/test/fixtures/rust-project/interface.json @@ -0,0 +1,83 @@ +{ + "name": "rust-project", + "types": { + "HttpRequest": { + "name": "HttpRequest", + "fields": { + "url": { + "type": "string" + }, + "method": { + "type": "string" + }, + "link": { + "type": "link", + "provider": "http" + } + } + }, + "HttpResponse": { + "name": "HttpResponse", + "fields": { + "body": { + "type": "string" + }, + "headers": { + "type": "map", + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + } + } + } + }, + "components": { + "add": { + "name": "add", + "inputs": { + "left": { + "type": "i64" + }, + "right": { + "type": "i64" + } + }, + "outputs": { + "sum": { + "type": "i64" + } + } + }, + "hello-world": { + "name": "hello-world", + "inputs": { + "message": { + "type": "string" + } + }, + "outputs": { + "greeting": { + "type": "string" + } + } + }, + "http-request": { + "name": "http-request", + "inputs": { + "request": { + "type": "ref", + "ref": "#/types/HttpRequest" + } + }, + "outputs": { + "response": { + "type": "ref", + "ref": "#/types/HttpResponse" + } + } + } + } +} \ No newline at end of file diff --git a/test/fixtures/rust-project/rust-toolchain.toml b/test/fixtures/rust-project/rust-toolchain.toml new file mode 100644 index 0000000..85e235c --- /dev/null +++ b/test/fixtures/rust-project/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +components = ["rustfmt", "clippy"] +targets = ["wasm32-unknown-unknown"] diff --git a/test/fixtures/rust-project/schemas/add.widl b/test/fixtures/rust-project/schemas/add.widl new file mode 100644 index 0000000..a6f85fd --- /dev/null +++ b/test/fixtures/rust-project/schemas/add.widl @@ -0,0 +1,13 @@ + +namespace "add" + +"Example inputs" +type Inputs { + left: i64, + right: i64 +} + +"Example outputs" +type Outputs { + sum: i64 +} \ No newline at end of file diff --git a/test/fixtures/rust-project/schemas/hello-world.widl b/test/fixtures/rust-project/schemas/hello-world.widl new file mode 100644 index 0000000..53b9942 --- /dev/null +++ b/test/fixtures/rust-project/schemas/hello-world.widl @@ -0,0 +1,13 @@ + +namespace "hello-world" + +"Example inputs" +type Inputs { + message: string +} + +"Example outputs" +type Outputs { + greeting: string +} + diff --git a/test/fixtures/rust-project/schemas/http-request.widl b/test/fixtures/rust-project/schemas/http-request.widl new file mode 100644 index 0000000..e0d1019 --- /dev/null +++ b/test/fixtures/rust-project/schemas/http-request.widl @@ -0,0 +1,12 @@ +namespace "http-request" + +import {HttpRequest,HttpResponse} from "./include/types.widl" + +type Inputs { + request: HttpRequest +} + +type Outputs { + response: HttpResponse +} + diff --git a/test/fixtures/rust-project/schemas/include/types.widl b/test/fixtures/rust-project/schemas/include/types.widl new file mode 100644 index 0000000..7c6b90a --- /dev/null +++ b/test/fixtures/rust-project/schemas/include/types.widl @@ -0,0 +1,10 @@ +type HttpResponse { + body:string + headers: {string: string} +} + +type HttpRequest { + url: string + method: string + link: link @provider("http") +} \ No newline at end of file diff --git a/test/fixtures/rust-project/src/components.rs b/test/fixtures/rust-project/src/components.rs new file mode 100644 index 0000000..63c2317 --- /dev/null +++ b/test/fixtures/rust-project/src/components.rs @@ -0,0 +1,354 @@ +/********************************************** +***** This file is generated, do not edit ***** +***********************************************/ + +pub use vino_provider::prelude::*; + +pub mod add; +pub mod hello_world; +pub mod http_request; + +type Result = std::result::Result; + +#[no_mangle] +pub(crate) extern "C" fn __guest_call(op_len: i32, req_len: i32) -> i32 { + use std::slice; + + let buf: Vec = Vec::with_capacity(req_len as _); + let req_ptr = buf.as_ptr(); + + let opbuf: Vec = Vec::with_capacity(op_len as _); + let op_ptr = opbuf.as_ptr(); + + let (slice, op) = unsafe { + wapc::__guest_request(op_ptr, req_ptr); + ( + slice::from_raw_parts(req_ptr, req_len as _), + slice::from_raw_parts(op_ptr, op_len as _), + ) + }; + + let op_str = ::std::str::from_utf8(op).unwrap(); + + match Dispatcher::dispatch(op_str, slice) { + Ok(response) => { + unsafe { wapc::__guest_response(response.as_ptr(), response.len()) } + 1 + } + Err(e) => { + let errmsg = e.to_string(); + unsafe { + wapc::__guest_error(errmsg.as_ptr(), errmsg.len() as _); + } + 0 + } + } +} + +static ALL_COMPONENTS: &[&str] = &["add", "hello-world", "http-request"]; + +pub struct Dispatcher {} +impl Dispatch for Dispatcher { + fn dispatch(op: &str, payload: &[u8]) -> CallResult { + let payload = IncomingPayload::from_buffer(payload)?; + let result = match op { + "add" => add::Component::default().execute(&payload), + "hello-world" => hello_world::Component::default().execute(&payload), + "http-request" => http_request::Component::default().execute(&payload), + _ => Err(WasmError::ComponentNotFound( + op.to_owned(), + ALL_COMPONENTS.join(", "), + )), + }?; + Ok(serialize(&result)?) + } +} + +pub mod types { + use vino_provider::prelude::*; + #[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize, Clone)] + pub struct HttpRequest { + #[serde(rename = "url")] + pub url: String, + #[serde(rename = "method")] + pub method: String, + #[serde(rename = "link")] + pub link: ProviderLink, + } + #[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize, Clone)] + pub struct HttpResponse { + #[serde(rename = "body")] + pub body: String, + #[serde(rename = "headers")] + pub headers: std::collections::HashMap, + } +} + +pub mod generated { + use super::*; + pub mod add { + use crate::components::add as implementation; + + pub use vino_provider::prelude::*; + + use super::*; + + #[derive(Default)] + pub struct Component {} + + impl WapcComponent for Component { + fn execute(&self, payload: &IncomingPayload) -> JobResult { + let outputs = get_outputs(payload.id()); + let inputs = populate_inputs(payload)?; + implementation::job(inputs, outputs) + } + } + + fn populate_inputs(payload: &IncomingPayload) -> Result { + Ok(Inputs { + left: deserialize(payload.get("left")?)?, + right: deserialize(payload.get("right")?)?, + }) + } + + impl From for TransportMap { + fn from(inputs: Inputs) -> TransportMap { + let mut map = TransportMap::new(); + map.insert("left".to_owned(), MessageTransport::success(&inputs.left)); + map.insert("right".to_owned(), MessageTransport::success(&inputs.right)); + map + } + } + + #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] + pub struct Inputs { + #[serde(rename = "left")] + pub left: i64, + #[serde(rename = "right")] + pub right: i64, + } + + #[derive(Debug)] + pub struct OutputPorts { + pub sum: SumSender, + } + + #[derive(Debug, PartialEq, Clone)] + pub struct SumSender { + id: u32, + } + + impl PortSender for SumSender { + type PayloadType = i64; + fn get_name(&self) -> String { + "sum".to_string() + } + fn get_id(&self) -> u32 { + self.id + } + } + + fn get_outputs(id: u32) -> OutputPorts { + OutputPorts { + sum: SumSender { id }, + } + } + + #[derive(Debug)] + pub struct Outputs { + packets: ProviderOutput, + } + + impl Outputs { + pub fn sum(&mut self) -> Result { + let packets = self + .packets + .take("sum") + .ok_or_else(|| ComponentError::new("No packets for port 'sum' found"))?; + Ok(PortOutput::new("sum".to_owned(), packets)) + } + } + + impl From for Outputs { + fn from(packets: ProviderOutput) -> Self { + Self { packets } + } + } + } + pub mod hello_world { + use crate::components::hello_world as implementation; + + pub use vino_provider::prelude::*; + + use super::*; + + #[derive(Default)] + pub struct Component {} + + impl WapcComponent for Component { + fn execute(&self, payload: &IncomingPayload) -> JobResult { + let outputs = get_outputs(payload.id()); + let inputs = populate_inputs(payload)?; + implementation::job(inputs, outputs) + } + } + + fn populate_inputs(payload: &IncomingPayload) -> Result { + Ok(Inputs { + message: deserialize(payload.get("message")?)?, + }) + } + + impl From for TransportMap { + fn from(inputs: Inputs) -> TransportMap { + let mut map = TransportMap::new(); + map.insert( + "message".to_owned(), + MessageTransport::success(&inputs.message), + ); + map + } + } + + #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] + pub struct Inputs { + #[serde(rename = "message")] + pub message: String, + } + + #[derive(Debug)] + pub struct OutputPorts { + pub greeting: GreetingSender, + } + + #[derive(Debug, PartialEq, Clone)] + pub struct GreetingSender { + id: u32, + } + + impl PortSender for GreetingSender { + type PayloadType = String; + fn get_name(&self) -> String { + "greeting".to_string() + } + fn get_id(&self) -> u32 { + self.id + } + } + + fn get_outputs(id: u32) -> OutputPorts { + OutputPorts { + greeting: GreetingSender { id }, + } + } + + #[derive(Debug)] + pub struct Outputs { + packets: ProviderOutput, + } + + impl Outputs { + pub fn greeting(&mut self) -> Result { + let packets = self + .packets + .take("greeting") + .ok_or_else(|| ComponentError::new("No packets for port 'greeting' found"))?; + Ok(PortOutput::new("greeting".to_owned(), packets)) + } + } + + impl From for Outputs { + fn from(packets: ProviderOutput) -> Self { + Self { packets } + } + } + } + pub mod http_request { + use crate::components::http_request as implementation; + + pub use vino_provider::prelude::*; + + use super::*; + + #[derive(Default)] + pub struct Component {} + + impl WapcComponent for Component { + fn execute(&self, payload: &IncomingPayload) -> JobResult { + let outputs = get_outputs(payload.id()); + let inputs = populate_inputs(payload)?; + implementation::job(inputs, outputs) + } + } + + fn populate_inputs(payload: &IncomingPayload) -> Result { + Ok(Inputs { + request: deserialize(payload.get("request")?)?, + }) + } + + impl From for TransportMap { + fn from(inputs: Inputs) -> TransportMap { + let mut map = TransportMap::new(); + map.insert( + "request".to_owned(), + MessageTransport::success(&inputs.request), + ); + map + } + } + + #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] + pub struct Inputs { + #[serde(rename = "request")] + pub request: super::types::HttpRequest, + } + + #[derive(Debug)] + pub struct OutputPorts { + pub response: ResponseSender, + } + + #[derive(Debug, PartialEq, Clone)] + pub struct ResponseSender { + id: u32, + } + + impl PortSender for ResponseSender { + type PayloadType = super::types::HttpResponse; + fn get_name(&self) -> String { + "response".to_string() + } + fn get_id(&self) -> u32 { + self.id + } + } + + fn get_outputs(id: u32) -> OutputPorts { + OutputPorts { + response: ResponseSender { id }, + } + } + + #[derive(Debug)] + pub struct Outputs { + packets: ProviderOutput, + } + + impl Outputs { + pub fn response(&mut self) -> Result { + let packets = self + .packets + .take("response") + .ok_or_else(|| ComponentError::new("No packets for port 'response' found"))?; + Ok(PortOutput::new("response".to_owned(), packets)) + } + } + + impl From for Outputs { + fn from(packets: ProviderOutput) -> Self { + Self { packets } + } + } + } +} diff --git a/test/fixtures/rust-project/src/components/add.rs b/test/fixtures/rust-project/src/components/add.rs new file mode 100644 index 0000000..4afe5ff --- /dev/null +++ b/test/fixtures/rust-project/src/components/add.rs @@ -0,0 +1,5 @@ +pub use crate::components::generated::add::*; + +pub(crate) fn job(input: Inputs, output: OutputPorts) -> JobResult { + Ok(()) +} diff --git a/test/fixtures/rust-project/src/components/hello_world.rs b/test/fixtures/rust-project/src/components/hello_world.rs new file mode 100644 index 0000000..54602c9 --- /dev/null +++ b/test/fixtures/rust-project/src/components/hello_world.rs @@ -0,0 +1,5 @@ +pub use crate::components::generated::hello_world::*; + +pub(crate) fn job(input: Inputs, output: OutputPorts) -> JobResult { + Ok(()) +} diff --git a/test/fixtures/rust-project/src/components/http.rs b/test/fixtures/rust-project/src/components/http.rs new file mode 100644 index 0000000..4448c62 --- /dev/null +++ b/test/fixtures/rust-project/src/components/http.rs @@ -0,0 +1,5 @@ +use crate::generated::http::*; + +pub(crate) fn job(input: Inputs, output: OutputPorts) -> JobResult { + Ok(()) +} diff --git a/test/fixtures/rust-project/src/components/http_request.rs b/test/fixtures/rust-project/src/components/http_request.rs new file mode 100644 index 0000000..8622483 --- /dev/null +++ b/test/fixtures/rust-project/src/components/http_request.rs @@ -0,0 +1,5 @@ +pub use crate::components::generated::http_request::*; + +pub(crate) fn job(input: Inputs, output: OutputPorts) -> JobResult { + Ok(()) +} diff --git a/test/fixtures/rust-project/src/components/my_component.rs b/test/fixtures/rust-project/src/components/my_component.rs new file mode 100644 index 0000000..f5b4c69 --- /dev/null +++ b/test/fixtures/rust-project/src/components/my_component.rs @@ -0,0 +1,5 @@ +use crate::generated::my_component::*; + +pub(crate) fn job(input: Inputs, output: OutputPorts) -> JobResult { + Ok(()) +} diff --git a/test/fixtures/rust-project/src/lib.rs b/test/fixtures/rust-project/src/lib.rs new file mode 100644 index 0000000..f188f2c --- /dev/null +++ b/test/fixtures/rust-project/src/lib.rs @@ -0,0 +1 @@ +pub mod components; diff --git a/test/json.test.ts b/test/json.test.ts index df2f053..b49c32c 100644 --- a/test/json.test.ts +++ b/test/json.test.ts @@ -49,7 +49,7 @@ describe('json command', function () { 'hello-world': { name: 'hello-world', inputs: { - message: { type: 'string' }, + messages: { type: 'list', element: { type: 'string' } }, }, outputs: { greeting: { type: 'string' },