sdml_tera/
lib.rs

1/*!
2Provides integration that allows document generation from SDML modules using the
3[Tera](https://keats.github.io/tera/docs/) template engine.
4
5This package provides a set of *rendering* functions as well as a set of *context* functions. By
6default all render functions will create new context value using the [`module_to_value`] function
7to convert a `Module` into a context object. However, you may provide your own context to add custom
8values.
9
10The documentation for the [`context`] module describes the simplifications made in the creation of
11the context object(s).
12
13# Example
14
15We wish to produce an output such as the following, a bulleted outline of a module.
16
17```markdown
18# Module `campaign` Outline
19
20* **campaign** (Module)
21  * **Name** <- *xsd:string* (Datatype)
22  * **CampaignId** <- *xsd:string* (Datatype)
23  * **State** (Enum)
24    * Running
25    * Paused
26    * error
27  * **Tag** (Structure)
28    * key -> *xsd:NMTOKEN*
29    * value -> *rdf:langString*
30  * **Ad** (Entity)
31  * **AdGroup** (Entity)
32  * **Campaign** (Entity)
33    * identity campaignId -> *CampaignId*
34    * name -> *unknown*
35    * tag -> *Tag*
36    * target -> *TargetCriteria*
37  * **AudienceTarget** (Entity)
38  * **GeographicTarget** (Entity)
39  * **TargetCriteria** (Union)
40    * Audience (Audience)
41    * Geographic (Geographic)
42```
43
44To do this we create a file named `outline.md` with the following content.
45
46```markdown
47{% macro member(item) %}
48{%- if item.__type == "reference" -%}
49*{{ item.type_ref }}*
50{% elif item.__type == "definition" -%}
51{{ item.name }} -> *{{ item.type_ref }}*
52{% endif -%}
53{% endmacro member %}
54
55# Module `{{ module.name }}` Outline
56
57* **{{ module.name }}** (Module)
58{% for def in module.definitions %}  * **{{ def.name }}**
59{%- if def.__type == "datatype" %} <- *{{ def.base_type }}*
60{%- endif %} ({{ def.__type | capitalize | replace(from="-", to=" ") }})
61{% if def.__type == "entity" -%}
62{%- if def.identity %}    * identity {{ self::member(item=def.identity) }}
63{%- endif -%}
64{%- if def.members -%}
65{% for member in def.members %}    * {{ self::member(item=member) }}
66{%- endfor -%}
67{%- endif -%}
68{%- elif def.__type == "enum" -%}
69{% for var in def.variants %}    * {{ var.name }}
70{% endfor -%}
71{% elif def.__type == "event" -%}
72{%- if def.members -%}
73{% for member in def.members %}    * {{ self::member(item=member) }}
74{%- endfor -%}
75{%- endif -%}
76{% elif def.__type == "structure" -%}
77{%- if def.identity %}  * identity {{ self::member(item=def.identity) }}
78{%- endif -%}
79{%- if def.members -%}
80{% for member in def.members %}    * {{ self::member(item=member) }}
81{%- endfor -%}
82{%- endif -%}
83{%- elif def.__type == "union" -%}
84{% for var in def.variants %}    * {% if var.rename %}{{ var.rename }} ({{ var.name }})
85{%- else %}{{ var.name }}
86{%- endif %}
87{% endfor -%}
88{% endif -%}
89{% endfor %}
90```
91
92Once we have finished testing using the `sdml-tera` tool we can write the following code to render
93any module with the template above.
94
95```rust
96use sdml_core::model::modules::Module;
97use sdml_core::store::ModuleStore;
98use sdml_tera::make_engine_from;
99use sdml_tera::render_module;
100
101fn print_module(module: &Module, cache: &impl ModuleStore) {
102
103    let engine = make_engine_from("tests/templates/**/*.md")
104        .expect("Could not parse template files");
105
106
107    let rendered = render_module(&engine, module, None, "outline.md")
108        .expect("Issue in template rendering");
109
110    println!("{}", rendered);
111}
112```
113
114# Features
115
116This crate also has a binary that allows you to test the development of templates. The tool takes a
117glob expression for Tera to load templates and a specific template name to use for a specific test.
118The input/output allows for file read/write and stdin/stdout, or for input you can specify a module
119name for the standard resolver to find for you.
120
121```bash
122❯ sdml-tera --help
123Simple Domain Modeling Language (SDML) Tera Integration
124
125Usage: sdml-tera [OPTIONS] --template-name <TEMPLATE_NAME> [MODULE]
126
127Arguments:
128  [MODULE]  SDML module, loaded using the standard resolver
129
130Options:
131  -o, --output <OUTPUT>                File name to write to, or '-' to write to stdout [default: -]
132  -i, --input <INPUT>                  Input SDML file name to read from, or '-' to read from stdin [default: -]
133  -g, --template-glob <TEMPLATE_GLOB>  [default: templates/**/*.md]
134  -n, --template-name <TEMPLATE_NAME>
135  -h, --help                           Print help
136  -V, --version                        Print version
137```
138
139The error messages produced by the tool are also verbose to help as much as possible to diagnose
140issues as you develop templates. For example, the following shows the output when a syntax error is
141found in a template.
142
143```bash
144An error occurred creating the Tera engine; most likely this is a syntax error in one of your templates.
145Error: A template error occurred; source:
146* Failed to parse "/Users/simonjo/Projects/sdm-lang/rust-sdml/sdml-tera/tests/templates/module.md"
147  --> 35:25
148   |
14935 | event {{ definition.name$ }} source {{ definition.source }}
150   |                         ^---
151   |
152   = expected `or`, `and`, `not`, `<=`, `>=`, `<`, `>`, `==`, `!=`, `+`, `-`, `*`, `/`, `%`, a filter, or a variable end (`}}`)
153```
154
155 */
156
157#![warn(
158    unknown_lints,
159    // ---------- Stylistic
160    absolute_paths_not_starting_with_crate,
161    elided_lifetimes_in_paths,
162    explicit_outlives_requirements,
163    macro_use_extern_crate,
164    nonstandard_style, /* group */
165    noop_method_call,
166    rust_2018_idioms,
167    single_use_lifetimes,
168    trivial_casts,
169    trivial_numeric_casts,
170    // ---------- Future
171    future_incompatible, /* group */
172    rust_2021_compatibility, /* group */
173    // ---------- Public
174    missing_debug_implementations,
175    // missing_docs,
176    unreachable_pub,
177    // ---------- Unsafe
178    unsafe_code,
179    unsafe_op_in_unsafe_fn,
180    // ---------- Unused
181    unused, /* group */
182)]
183#![deny(
184    // ---------- Public
185    exported_private_dependencies,
186    // ---------- Deprecated
187    anonymous_parameters,
188    bare_trait_objects,
189    ellipsis_inclusive_range_patterns,
190    // ---------- Unsafe
191    deref_nullptr,
192    drop_bounds,
193    dyn_drop,
194)]
195
196use sdml_core::model::modules::Module;
197use sdml_errors::Error;
198use sdml_json::write::{module_list_to_value, module_to_value, ValueOptions};
199use std::{fs::OpenOptions, io::Write, path::Path};
200use tera::{Context, Tera};
201
202// ------------------------------------------------------------------------------------------------
203// Private Macros
204// ------------------------------------------------------------------------------------------------
205
206// ------------------------------------------------------------------------------------------------
207// Public Functions > Engine
208// ------------------------------------------------------------------------------------------------
209
210///
211/// A local wrapper around the Tera engine creation.
212///
213/// This function is introduced mainly to allow the integration with the SDML core error structure.
214///
215#[inline]
216pub fn make_engine_from(glob: &str) -> Result<Tera, Error> {
217    let mut engine = Tera::new(glob)?;
218    add_ons::register(&mut engine)?;
219    Ok(engine)
220}
221
222// ------------------------------------------------------------------------------------------------
223// Public Functions > Render Single Module
224// ------------------------------------------------------------------------------------------------
225
226///
227/// Render `module`, with the template in the file `template_name`, and using `engine`.
228///
229/// If `context` is not specified a new blank object is created, and in either case a representation
230/// of the module is added to the context object under the key `"module"`.
231///
232/// ```json
233/// {
234///     "module": {}
235/// }
236/// ```
237///
238/// In the case of this function the result is returned as a `String`.
239///
240pub fn render_module(
241    engine: &Tera,
242    module: &Module,
243    context: Option<Context>,
244    template_name: &str,
245) -> Result<String, Error> {
246    let context = make_context_from(module, context);
247    let result = engine.render(template_name, &context)?;
248    Ok(result)
249}
250
251///
252/// Render `module`, with the template in the file `template_name`, and using `engine`.
253///
254/// If `context` is not specified a new blank object is created, and in either case a representation
255/// of the module is added to the context object under the key `"module"`.
256///
257/// ```json
258/// {
259///     "module": {}
260/// }
261/// ```
262///
263/// In the case of this function the template is rendered to the write implementation `w`.
264///
265pub fn render_module_to<W: Write>(
266    engine: &Tera,
267    module: &Module,
268    context: Option<Context>,
269    template_name: &str,
270    w: &mut W,
271) -> Result<(), Error> {
272    let context = make_context_from(module, context);
273    engine.render_to(template_name, &context, w)?;
274    Ok(())
275}
276
277///
278/// Render `module`, with the template in the file `template_name`, and using `engine`.
279///
280/// If `context` is not specified a new blank object is created, and in either case a representation
281/// of the module is added to the context object under the key `"module"`.
282///
283/// ```json
284/// {
285///     "module": {}
286/// }
287/// ```
288///
289/// In the case of this function the template is rendered to the file named by `path`.
290///
291pub fn render_module_to_file<P: AsRef<Path>>(
292    engine: &Tera,
293    module: &Module,
294    context: Option<Context>,
295    template_name: &str,
296    path: P,
297) -> Result<(), Error> {
298    let mut file = OpenOptions::new()
299        .write(true)
300        .truncate(true)
301        .open(path.as_ref())?;
302
303    render_module_to(engine, module, context, template_name, &mut file)?;
304    Ok(())
305}
306
307// ------------------------------------------------------------------------------------------------
308// Public Functions > Render Set of Modules
309// ------------------------------------------------------------------------------------------------
310
311///
312/// Render the set of `modules`, with the template in the file `template_name`, and using `engine`.
313///
314/// If `context` is not specified a new blank object is created, and in either case a map is created
315/// under the key `"modules"` as a map from module name to module representation.
316///
317/// ```json
318/// {
319///     "modules": {
320///         "Identifier": {},
321///     }
322/// }
323/// ```
324///
325/// In the case of this function the result is returned as a `String`.
326///
327pub fn render_modules(
328    engine: &Tera,
329    modules: Vec<&Module>,
330    context: Option<Context>,
331    template_name: &str,
332) -> Result<String, Error> {
333    let context = make_context_from_all(modules, context);
334    let result = engine.render(template_name, &context)?;
335    Ok(result)
336}
337
338///
339/// Render the set of `modules`, with the template in the file `template_name`, and using `engine`.
340/// If `context` is not specified a new blank object is created, and in either case a map is created
341/// under the key `"modules"` as a map from module name to module representation.
342///
343/// ```json
344/// {
345///     "modules": {
346///         "Identifier": {},
347///     }
348/// }
349/// ```
350///
351/// In the case of this function the template is rendered to the write implementation `w`.
352///
353pub fn render_modules_to<W: Write>(
354    engine: &Tera,
355    modules: Vec<&Module>,
356    context: Option<Context>,
357    template_name: &str,
358    w: &mut W,
359) -> Result<(), Error> {
360    let context = make_context_from_all(modules, context);
361    engine.render_to(template_name, &context, w)?;
362    Ok(())
363}
364
365///
366/// Render the set of `modules`, with the template in the file `template_name`, and using `engine`.
367///
368/// If `context` is not specified a new blank object is created, and in either case a map is created
369/// under the key `"modules"` as a map from module name to module representation.
370///
371/// ```json
372/// {
373///     "modules": {
374///         "Identifier": {},
375///     }
376/// }
377/// ```
378///
379/// In the case of this function the template is rendered to the file named by `path`.
380///
381pub fn render_modules_to_file<P: AsRef<Path>>(
382    engine: &Tera,
383    modules: Vec<&Module>,
384    context: Option<Context>,
385    template_name: &str,
386    path: P,
387) -> Result<(), Error> {
388    let mut file = OpenOptions::new()
389        .write(true)
390        .truncate(true)
391        .open(path.as_ref())?;
392    render_modules_to(engine, modules, context, template_name, &mut file)?;
393    Ok(())
394}
395
396// ------------------------------------------------------------------------------------------------
397// Private Functions
398// ------------------------------------------------------------------------------------------------
399
400fn make_context_from(module: &Module, context: Option<Context>) -> Context {
401    let value = module_to_value(
402        module,
403        ValueOptions::default()
404            .emit_context_only(true)
405            .with_spans_included(true),
406    );
407
408    let mut context = context.unwrap_or_default();
409    context.insert("module", &value);
410    context
411}
412
413fn make_context_from_all(modules: Vec<&Module>, context: Option<Context>) -> Context {
414    let values = module_list_to_value(
415        &modules,
416        ValueOptions::default()
417            .emit_context_only(true)
418            .with_spans_included(true),
419    );
420
421    let mut context = context.unwrap_or_default();
422    context.insert("modules", &values);
423    context
424}
425
426// ------------------------------------------------------------------------------------------------
427// Modules
428// ------------------------------------------------------------------------------------------------
429
430mod add_ons;