diff --git a/core/src/hir/attrs.rs b/core/src/hir/attrs.rs index a89ab7027..c7c226190 100644 --- a/core/src/hir/attrs.rs +++ b/core/src/hir/attrs.rs @@ -81,6 +81,16 @@ pub struct DemoInfo { /// `#[diplomat::demo(external)]` represents an item that we will not evaluate, and should be passed to the rendering engine to provide. pub external: bool, + /// `#[diplomat::demo(custom_func = "/file/name/here.mjs")]` can be used above any `struct` definition in the bridge. The linked `.mjs` should contain a JS definition of functions that should be bundled with demo_gen's output. + /// + /// We call these functions "custom functions", as they are JS functions that are not automagically generated by demo_gen, but rather included as part of its JS output in the `RenderInfo` object. + /// + /// For more information on custom functions (and their use), see the relevant chapter in [the book](https://rust-diplomat.github.io/book/demo_gen/custom_functions.html). + /// + /// Files are located relative to lib.rs. + /// + pub custom_func: Option, + /// `#[diplomat::demo(input(...))]` represents configuration options for anywhere we might expect user input. pub input_cfg: DemoInputCFG, } @@ -405,11 +415,29 @@ impl Attrs { } }) .expect("Could not read input(...)"); + } else if path_ident == "custom_func" { + let v = &attr.meta.require_name_value().unwrap().value; + + if let syn::Expr::Lit(s) = v { + if let syn::Lit::Str(string) = &s.lit { + this.demo_attrs.custom_func = Some(string.value()); + } else { + errors.push(LoweringError::Other(format!( + "#[diplomat::demo(custom_func={s:?}) must be a literal string." + ))); + } + } else { + errors.push(LoweringError::Other(format!( + "#[diplomat::demo(custom_func={v:?}) must be a literal string." + ))); + } } else { - panic!("Unknown demo_attr: {path_ident:?}"); + errors.push(LoweringError::Other(format!( + "Unknown demo_attr: {path_ident:?}" + ))); } } else { - panic!("Unknown demo_attr: {path:?}"); + errors.push(LoweringError::Other(format!("Unknown demo_attr: {path:?}"))); } } diff --git a/core/src/hir/snapshots/diplomat_core__hir__elision__tests__elision_in_struct.snap b/core/src/hir/snapshots/diplomat_core__hir__elision__tests__elision_in_struct.snap index 122d3c0af..3fec47a51 100644 --- a/core/src/hir/snapshots/diplomat_core__hir__elision__tests__elision_in_struct.snap +++ b/core/src/hir/snapshots/diplomat_core__hir__elision__tests__elision_in_struct.snap @@ -48,6 +48,7 @@ Method { generate: false, default_constructor: false, external: false, + custom_func: None, input_cfg: DemoInputCFG { label: "", default_value: "", @@ -100,6 +101,7 @@ Method { generate: false, default_constructor: false, external: false, + custom_func: None, input_cfg: DemoInputCFG { label: "", default_value: "", @@ -125,6 +127,7 @@ Method { generate: false, default_constructor: false, external: false, + custom_func: None, input_cfg: DemoInputCFG { label: "", default_value: "", diff --git a/core/src/hir/snapshots/diplomat_core__hir__elision__tests__simple_mod.snap b/core/src/hir/snapshots/diplomat_core__hir__elision__tests__simple_mod.snap index b1e7c744c..a19cedd5f 100644 --- a/core/src/hir/snapshots/diplomat_core__hir__elision__tests__simple_mod.snap +++ b/core/src/hir/snapshots/diplomat_core__hir__elision__tests__simple_mod.snap @@ -51,6 +51,7 @@ TypeContext { generate: false, default_constructor: false, external: false, + custom_func: None, input_cfg: DemoInputCFG { label: "", default_value: "", @@ -107,6 +108,7 @@ TypeContext { generate: false, default_constructor: false, external: false, + custom_func: None, input_cfg: DemoInputCFG { label: "", default_value: "", @@ -151,6 +153,7 @@ TypeContext { generate: false, default_constructor: false, external: false, + custom_func: None, input_cfg: DemoInputCFG { label: "", default_value: "", @@ -173,6 +176,7 @@ TypeContext { generate: false, default_constructor: false, external: false, + custom_func: None, input_cfg: DemoInputCFG { label: "", default_value: "", @@ -236,6 +240,7 @@ TypeContext { generate: false, default_constructor: false, external: false, + custom_func: None, input_cfg: DemoInputCFG { label: "", default_value: "", @@ -294,6 +299,7 @@ TypeContext { generate: false, default_constructor: false, external: false, + custom_func: None, input_cfg: DemoInputCFG { label: "", default_value: "", @@ -331,6 +337,7 @@ TypeContext { generate: false, default_constructor: false, external: false, + custom_func: None, input_cfg: DemoInputCFG { label: "", default_value: "", @@ -369,6 +376,7 @@ TypeContext { generate: false, default_constructor: false, external: false, + custom_func: None, input_cfg: DemoInputCFG { label: "", default_value: "", @@ -391,6 +399,7 @@ TypeContext { generate: false, default_constructor: false, external: false, + custom_func: None, input_cfg: DemoInputCFG { label: "", default_value: "", @@ -436,6 +445,7 @@ TypeContext { generate: false, default_constructor: false, external: false, + custom_func: None, input_cfg: DemoInputCFG { label: "", default_value: "", diff --git a/example/demo_gen/custom_func/a.mjs b/example/demo_gen/custom_func/a.mjs new file mode 100644 index 000000000..fedec928a --- /dev/null +++ b/example/demo_gen/custom_func/a.mjs @@ -0,0 +1,20 @@ +import { lib } from "./index.mjs"; + +export function multiplyPow10(power) { + let fixedDecimal = lib.FixedDecimal.new_(10); + fixedDecimal.multiplyPow10(power); + return fixedDecimal.toString(); +} + +export default { + "FixedDecimal.multiplyPow10": { + func: multiplyPow10, + funcName: "FixedDecimal.multiplyPow10", + parameters: [ + { + name: "power", + type: "number" + } + ] + } +}; \ No newline at end of file diff --git a/example/demo_gen/demo/a.mjs b/example/demo_gen/demo/a.mjs new file mode 100644 index 000000000..fedec928a --- /dev/null +++ b/example/demo_gen/demo/a.mjs @@ -0,0 +1,20 @@ +import { lib } from "./index.mjs"; + +export function multiplyPow10(power) { + let fixedDecimal = lib.FixedDecimal.new_(10); + fixedDecimal.multiplyPow10(power); + return fixedDecimal.toString(); +} + +export default { + "FixedDecimal.multiplyPow10": { + func: multiplyPow10, + funcName: "FixedDecimal.multiplyPow10", + parameters: [ + { + name: "power", + type: "number" + } + ] + } +}; \ No newline at end of file diff --git a/example/demo_gen/demo/index.mjs b/example/demo_gen/demo/index.mjs index 86c60fe1f..40071ac6a 100644 --- a/example/demo_gen/demo/index.mjs +++ b/example/demo_gen/demo/index.mjs @@ -4,53 +4,57 @@ export * as FixedDecimalFormatterDemo from "./FixedDecimalFormatter.mjs"; import * as FixedDecimalDemo from "./FixedDecimal.mjs"; export * as FixedDecimalDemo from "./FixedDecimal.mjs"; +import RenderTerminiFixedDecimal from "./a.mjs"; + + +let termini = Object.assign({ + "FixedDecimalFormatter.formatWrite": { + func: FixedDecimalFormatterDemo.formatWrite, + // For avoiding webpacking minifying issues: + funcName: "FixedDecimalFormatter.formatWrite", + parameters: [ + + { + name: "Locale Name", + type: "string" + }, + + { + name: "ICU4X Fixed Decimal Grouping Strategy", + type: "FixedDecimalGroupingStrategy" + }, + + { + name: "Useless Config (Ignore)", + type: "boolean", + defaultValue: "true" + }, + + { + name: "ICU4XFixedDecimal Value", + type: "number", + defaultValue: "1000" + } + + ] + }, + + "FixedDecimal.toString": { + func: FixedDecimalDemo.toString, + // For avoiding webpacking minifying issues: + funcName: "FixedDecimal.toString", + parameters: [ + + { + name: "ICU4XFixedDecimal Value", + type: "number", + defaultValue: "1000" + } + + ] + } +}, RenderTerminiFixedDecimal); export const RenderInfo = { - termini: { - "FixedDecimalFormatter.formatWrite": { - func: FixedDecimalFormatterDemo.formatWrite, - // For avoiding webpacking minifying issues: - funcName: "FixedDecimalFormatter.formatWrite", - parameters: [ - - { - name: "Locale Name", - type: "string" - }, - - { - name: "ICU4X Fixed Decimal Grouping Strategy", - type: "FixedDecimalGroupingStrategy" - }, - - { - name: "Useless Config (Ignore)", - type: "boolean", - defaultValue: "true" - }, - - { - name: "ICU4XFixedDecimal Value", - type: "number", - defaultValue: "1000" - } - - ] - }, - - "FixedDecimal.toString": { - func: FixedDecimalDemo.toString, - // For avoiding webpacking minifying issues: - funcName: "FixedDecimal.toString", - parameters: [ - - { - name: "ICU4XFixedDecimal Value", - type: "number", - defaultValue: "1000" - } - - ] - } - }, + "termini": termini }; \ No newline at end of file diff --git a/example/demo_gen/test/test-demo.mjs b/example/demo_gen/test/test-demo.mjs index f251e1b15..858cbd0e8 100644 --- a/example/demo_gen/test/test-demo.mjs +++ b/example/demo_gen/test/test-demo.mjs @@ -1,5 +1,5 @@ import test from "ava"; -import { FixedDecimalDemo, FixedDecimalFormatterDemo } from "mini-icu4x-demo"; +import { FixedDecimalDemo, FixedDecimalFormatterDemo, RenderInfo } from "mini-icu4x-demo"; import { FixedDecimalGroupingStrategy } from "mini-icu4x"; @@ -9,4 +9,8 @@ test("Test FixedDecimal", (t) => { test("Test FixedDecimalFormatter", (t) => { t.is(FixedDecimalFormatterDemo.formatWrite("en", FixedDecimalGroupingStrategy.Always, false, 1000), "1,000"); +}); + +test("Custom Function", (t) => { + t.is(RenderInfo.termini["FixedDecimal.multiplyPow10"].func(3), "10000"); }); \ No newline at end of file diff --git a/example/src/fixed_decimal.rs b/example/src/fixed_decimal.rs index ed6055924..3919739ea 100644 --- a/example/src/fixed_decimal.rs +++ b/example/src/fixed_decimal.rs @@ -7,6 +7,9 @@ pub mod ffi { #[diplomat::opaque] #[diplomat::rust_link(fixed_decimal::FixedDecimal, Struct)] + // Link to where other custom functions for this class can be found. + // Make sure any .mjs file export defaults an object that matches the `RenderTerminus.terminus` object in content. + #[diplomat::demo(custom_func = "../demo_gen/custom_func/a.mjs")] pub struct FixedDecimal(pub fixed_decimal::FixedDecimal); impl FixedDecimal { diff --git a/feature_tests/demo_gen/demo/index.mjs b/feature_tests/demo_gen/demo/index.mjs index 16f1d18bb..a8c9c2b2c 100644 --- a/feature_tests/demo_gen/demo/index.mjs +++ b/feature_tests/demo_gen/demo/index.mjs @@ -11,85 +11,88 @@ import * as Utf16WrapDemo from "./Utf16Wrap.mjs"; export * as Utf16WrapDemo from "./Utf16Wrap.mjs"; + +let termini = Object.assign({ + "OptionString.write": { + func: OptionStringDemo.write, + // For avoiding webpacking minifying issues: + funcName: "OptionString.write", + parameters: [ + + { + name: "DiplomatStr", + type: "string" + } + + ] + }, + + "Float64Vec.toString": { + func: Float64VecDemo.toString, + // For avoiding webpacking minifying issues: + funcName: "Float64Vec.toString", + parameters: [ + + { + name: "V", + type: "Array" + } + + ] + }, + + "MyString.getStr": { + func: MyStringDemo.getStr, + // For avoiding webpacking minifying issues: + funcName: "MyString.getStr", + parameters: [ + + { + name: "V", + type: "string" + } + + ] + }, + + "MyString.stringTransform": { + func: MyStringDemo.stringTransform, + // For avoiding webpacking minifying issues: + funcName: "MyString.stringTransform", + parameters: [ + + { + name: "Foo", + type: "string" + } + + ] + }, + + "Opaque.getDebugStr": { + func: OpaqueDemo.getDebugStr, + // For avoiding webpacking minifying issues: + funcName: "Opaque.getDebugStr", + parameters: [ + + ] + }, + + "Utf16Wrap.getDebugStr": { + func: Utf16WrapDemo.getDebugStr, + // For avoiding webpacking minifying issues: + funcName: "Utf16Wrap.getDebugStr", + parameters: [ + + { + name: "Input", + type: "string" + } + + ] + } +}); + export const RenderInfo = { - termini: { - "OptionString.write": { - func: OptionStringDemo.write, - // For avoiding webpacking minifying issues: - funcName: "OptionString.write", - parameters: [ - - { - name: "DiplomatStr", - type: "string" - } - - ] - }, - - "Float64Vec.toString": { - func: Float64VecDemo.toString, - // For avoiding webpacking minifying issues: - funcName: "Float64Vec.toString", - parameters: [ - - { - name: "V", - type: "Array" - } - - ] - }, - - "MyString.getStr": { - func: MyStringDemo.getStr, - // For avoiding webpacking minifying issues: - funcName: "MyString.getStr", - parameters: [ - - { - name: "V", - type: "string" - } - - ] - }, - - "MyString.stringTransform": { - func: MyStringDemo.stringTransform, - // For avoiding webpacking minifying issues: - funcName: "MyString.stringTransform", - parameters: [ - - { - name: "Foo", - type: "string" - } - - ] - }, - - "Opaque.getDebugStr": { - func: OpaqueDemo.getDebugStr, - // For avoiding webpacking minifying issues: - funcName: "Opaque.getDebugStr", - parameters: [ - - ] - }, - - "Utf16Wrap.getDebugStr": { - func: Utf16WrapDemo.getDebugStr, - // For avoiding webpacking minifying issues: - funcName: "Utf16Wrap.getDebugStr", - parameters: [ - - { - name: "Input", - type: "string" - } - - ] - } - }, + "termini": termini }; \ No newline at end of file diff --git a/tool/src/demo_gen/mod.rs b/tool/src/demo_gen/mod.rs index 510f9b5b6..9bb72e263 100644 --- a/tool/src/demo_gen/mod.rs +++ b/tool/src/demo_gen/mod.rs @@ -56,6 +56,7 @@ pub(crate) struct DemoConfig { /// This JS should include: /// Render Termini that can be called, and internal functions to construct dependencies that the Render Terminus function needs. pub(crate) fn run<'tcx>( + entry: &std::path::Path, tcx: &'tcx TypeContext, docs: &'tcx diplomat_core::ast::DocsUrlGenerator, conf: Option, @@ -64,6 +65,8 @@ pub(crate) fn run<'tcx>( let errors = ErrorStore::default(); let files = FileMap::default(); + let root = entry.parent().unwrap(); + let unwrapped_conf = conf.unwrap_or_default(); let import_path_exists = @@ -89,12 +92,18 @@ pub(crate) fn run<'tcx>( termini_exports: Vec, pub termini: Vec, pub js_out: String, + + pub imports: Vec, + pub custom_func_objs: Vec, } let mut out_info = IndexInfo { termini_exports: Vec::new(), termini: Vec::new(), js_out: format!("{import_path}{module_name}"), + + imports: Vec::new(), + custom_func_objs: Vec::new(), }; let is_explicit = unwrapped_conf.explicit_generation.unwrap_or(false); @@ -109,17 +118,60 @@ pub(crate) fn run<'tcx>( let mut termini = Vec::new(); { - let type_name = formatter.fmt_type_name(id); + let ty_name = formatter.fmt_type_name(id); + let type_name: String = ty_name.into(); + + let js_file_name = + formatter.fmt_file_name(&type_name.clone(), &crate::js::FileType::Module); let ty = tcx.resolve_type(id); - if ty.attrs().disable { + + let attrs = ty.attrs(); + if attrs.disable { continue; } + if let Some(custom_func) = &attrs.demo_attrs.custom_func { + let custom_func_filename = custom_func.to_string(); + + let file_path = root.join(custom_func_filename.clone()); + + let file_name: String = + String::from(file_path.file_name().unwrap().to_str().unwrap()); + + // Copy the custom function file from where it is relative to the FFI definition to our output directory. + let read = std::fs::read(file_path.clone()); + + if let Ok(r) = read { + let from_utf = String::from_utf8(r); + if let Ok(contents) = from_utf { + files.add_file(file_name.clone(), contents); + } else if let Err(e) = from_utf { + errors.push_error(format!( + "Could not convert contents of {custom_func_filename} to UTF-8: {e}" + )); + continue; + } + } else if let Err(e) = read { + errors.push_error(format!("Could not read {custom_func_filename} as a custom function file path ({file_path:?}): {e}")); + continue; + } + + // Then add it to our imports for `index.mjs`: + out_info.imports.push(format!( + r#"import RenderTermini{type_name} from "./{file_name}";"# + )); + + // Finally, make sure the user-defined RenderTermini is added to the terminus object: + out_info + .custom_func_objs + .push(format!("RenderTermini{type_name}")); + } + for method in methods { if method.attrs.disable - || !RenderTerminusContext::is_valid_terminus(method) || (is_explicit && !method.attrs.demo_attrs.generate) + || !RenderTerminusContext::is_valid_terminus(method) { continue; } @@ -127,18 +179,19 @@ pub(crate) fn run<'tcx>( let _guard = errors .set_context_method(ty.name().as_str().into(), method.name.as_str().into()); + let function_name = formatter.fmt_method_name(method); + let mut ctx = RenderTerminusContext { tcx, formatter: &formatter, errors: &errors, terminus_info: TerminusInfo { - function_name: formatter.fmt_method_name(method), + function_name: function_name.clone(), out_params: Vec::new(), - type_name: type_name.clone().into(), + type_name: type_name.clone(), - js_file_name: formatter - .fmt_file_name(&type_name, &crate::js::FileType::Module), + js_file_name: js_file_name.clone(), node_call_stack: String::default(), @@ -152,7 +205,7 @@ pub(crate) fn run<'tcx>( module_name: module_name.clone(), }; - ctx.evaluate(type_name.clone().into(), method); + ctx.evaluate(type_name.clone(), method); termini.push(ctx.terminus_info); } diff --git a/tool/src/lib.rs b/tool/src/lib.rs index f5cf60afb..11adb8e6c 100644 --- a/tool/src/lib.rs +++ b/tool/src/lib.rs @@ -96,7 +96,7 @@ pub fn gen( silent, )?; } - demo_gen::run(&tcx, docs_url_gen, conf) + demo_gen::run(entry, &tcx, docs_url_gen, conf) } "kotlin" => kotlin::run(&tcx, library_config), o => panic!("Unknown target: {}", o), diff --git a/tool/templates/demo_gen/index.js.jinja b/tool/templates/demo_gen/index.js.jinja index 470b50340..bdd2fed5c 100644 --- a/tool/templates/demo_gen/index.js.jinja +++ b/tool/templates/demo_gen/index.js.jinja @@ -3,27 +3,32 @@ export * as lib from "{{js_out}}"; import * as {{terminus.type_name}}Demo from "./{{terminus.js_file_name}}"; export * as {{terminus.type_name}}Demo from "./{{terminus.js_file_name}}"; {% endfor %} +{% for i in imports -%} +{{ i }} +{% endfor %} + +let termini = Object.assign({ +{%- for terminus in termini %} + "{{terminus.type_name}}.{{terminus.function_name}}": { + func: {{terminus.type_name}}Demo.{{terminus.function_name}}, + // For avoiding webpacking minifying issues: + funcName: "{{terminus.type_name}}.{{terminus.function_name}}", + parameters: [ + {% for param in terminus.out_params %} + { + name: "{{param.label}}", + type: "{{param.type_name}}" + {%- if !param.default_value.is_empty() -%} + , + defaultValue: "{{ param.default_value }}" + {%- endif %} + }{% if !loop.last %},{% endif %} + {% endfor %} + ] + }{% if !loop.last %},{% endif %} +{% endfor -%} +}{% for obj in custom_func_objs %}{% if loop.first %}, {% endif %}{{obj}}{% if !loop.last %}, {% endif %}{% endfor %}); export const RenderInfo = { - termini: { - {%- for terminus in termini %} - "{{terminus.type_name}}.{{terminus.function_name}}": { - func: {{terminus.type_name}}Demo.{{terminus.function_name}}, - // For avoiding webpacking minifying issues: - funcName: "{{terminus.type_name}}.{{terminus.function_name}}", - parameters: [ - {% for param in terminus.out_params %} - { - name: "{{param.label}}", - type: "{{param.type_name}}" - {%- if !param.default_value.is_empty() -%} - , - defaultValue: "{{ param.default_value }}" - {%- endif %} - }{% if !loop.last %},{% endif %} - {% endfor %} - ] - }{% if !loop.last %},{% endif %} - {% endfor -%} - }, + "termini": termini }; \ No newline at end of file