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;