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;