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, cache, 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 context::module_to_value;
197use sdml_core::{model::modules::Module, store::ModuleStore};
198use sdml_errors::Error;
199use std::{fs::OpenOptions, io::Write, path::Path};
200use tera::{Context, Map, Tera, Value};
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 engine = Tera::new(glob)?;
218    Ok(engine)
219}
220
221// ------------------------------------------------------------------------------------------------
222// Public Functions > Render Single Module
223// ------------------------------------------------------------------------------------------------
224
225///
226/// Render `module`, with the template in the file `template_name`, and using `engine`.
227///
228/// If `context` is not specified a new blank object is created, and in either case a representation
229/// of the module is added to the context object under the key `"module"`.
230///
231/// ```json
232/// {
233///     "module": {}
234/// }
235/// ```
236///
237/// In the case of this function the result is returned as a `String`.
238///
239pub fn render_module(
240    engine: &Tera,
241    module: &Module,
242    cache: &impl ModuleStore,
243    context: Option<Context>,
244    template_name: &str,
245) -> Result<String, Error> {
246    let context = make_context_from(module, cache, 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    cache: &impl ModuleStore,
269    context: Option<Context>,
270    template_name: &str,
271    w: &mut W,
272) -> Result<(), Error> {
273    let context = make_context_from(module, cache, context);
274    engine.render_to(template_name, &context, w)?;
275    Ok(())
276}
277
278///
279/// Render `module`, with the template in the file `template_name`, and using `engine`.
280///
281/// If `context` is not specified a new blank object is created, and in either case a representation
282/// of the module is added to the context object under the key `"module"`.
283///
284/// ```json
285/// {
286///     "module": {}
287/// }
288/// ```
289///
290/// In the case of this function the template is rendered to the file named by `path`.
291///
292pub fn render_module_to_file<P: AsRef<Path>>(
293    engine: &Tera,
294    module: &Module,
295    cache: &impl ModuleStore,
296    context: Option<Context>,
297    template_name: &str,
298    path: P,
299) -> Result<(), Error> {
300    let mut file = OpenOptions::new()
301        .write(true)
302        .truncate(true)
303        .open(path.as_ref())?;
304
305    render_module_to(engine, module, cache, context, template_name, &mut file)?;
306    Ok(())
307}
308
309// ------------------------------------------------------------------------------------------------
310// Public Functions > Render Set of Modules
311// ------------------------------------------------------------------------------------------------
312
313///
314/// Render the set of `modules`, with the template in the file `template_name`, and using `engine`.
315///
316/// If `context` is not specified a new blank object is created, and in either case a map is created
317/// under the key `"modules"` as a map from module name to module representation.
318///
319/// ```json
320/// {
321///     "modules": {
322///         "Identifier": {},
323///     }
324/// }
325/// ```
326///
327/// In the case of this function the result is returned as a `String`.
328///
329pub fn render_modules(
330    engine: &Tera,
331    modules: Vec<&Module>,
332    cache: &impl ModuleStore,
333    context: Option<Context>,
334    template_name: &str,
335) -> Result<String, Error> {
336    let context = make_context_from_all(modules, cache, context);
337    let result = engine.render(template_name, &context)?;
338    Ok(result)
339}
340
341///
342/// Render the set of `modules`, with the template in the file `template_name`, and using `engine`.
343/// If `context` is not specified a new blank object is created, and in either case a map is created
344/// under the key `"modules"` as a map from module name to module representation.
345///
346/// ```json
347/// {
348///     "modules": {
349///         "Identifier": {},
350///     }
351/// }
352/// ```
353///
354/// In the case of this function the template is rendered to the write implementation `w`.
355///
356pub fn render_modules_to<W: Write>(
357    engine: &Tera,
358    modules: Vec<&Module>,
359    cache: &impl ModuleStore,
360    context: Option<Context>,
361    template_name: &str,
362    w: &mut W,
363) -> Result<(), Error> {
364    let context = make_context_from_all(modules, cache, context);
365    engine.render_to(template_name, &context, w)?;
366    Ok(())
367}
368
369///
370/// Render the set of `modules`, with the template in the file `template_name`, and using `engine`.
371///
372/// If `context` is not specified a new blank object is created, and in either case a map is created
373/// under the key `"modules"` as a map from module name to module representation.
374///
375/// ```json
376/// {
377///     "modules": {
378///         "Identifier": {},
379///     }
380/// }
381/// ```
382///
383/// In the case of this function the template is rendered to the file named by `path`.
384///
385pub fn render_modules_to_file<P: AsRef<Path>>(
386    engine: &Tera,
387    modules: Vec<&Module>,
388    cache: &impl ModuleStore,
389    context: Option<Context>,
390    template_name: &str,
391    path: P,
392) -> Result<(), Error> {
393    let mut file = OpenOptions::new()
394        .write(true)
395        .truncate(true)
396        .open(path.as_ref())?;
397    render_modules_to(engine, modules, cache, context, template_name, &mut file)?;
398    Ok(())
399}
400
401// ------------------------------------------------------------------------------------------------
402// Private Functions
403// ------------------------------------------------------------------------------------------------
404
405fn make_context_from(
406    module: &Module,
407    cache: &impl ModuleStore,
408    context: Option<Context>,
409) -> Context {
410    let (_, value) = module_to_value(module, cache);
411
412    let mut context = context.unwrap_or_default();
413    context.insert("module", &value);
414    context
415}
416
417fn make_context_from_all(
418    modules: Vec<&Module>,
419    cache: &impl ModuleStore,
420    context: Option<Context>,
421) -> Context {
422    let values: Map<String, Value> = modules
423        .iter()
424        .map(|module| module_to_value(module, cache))
425        .collect();
426
427    let mut context = context.unwrap_or_default();
428    context.insert("modules", &values);
429    context
430}
431
432// ------------------------------------------------------------------------------------------------
433// Modules
434// ------------------------------------------------------------------------------------------------
435
436pub mod context;