weld_codegen/
render.rs

1//! Code generation
2//!
3use std::str::FromStr;
4
5use atelier_core::model::{Identifier, NamespaceID, ShapeID};
6pub use handlebars::RenderError;
7use handlebars::{
8    Context, Handlebars, Helper, HelperDef, HelperResult, Output, RenderContext, ScopedJson,
9};
10use serde::Serialize;
11use serde_json::Value;
12
13use crate::{strings, JsonMap, JsonValue};
14
15// these defaults can be overridden by the config file
16
17const DOCUMENTATION_TRAIT: &str = "smithy.api#documentation";
18const TRAIT_TRAIT: &str = "smithy.api#trait";
19
20/// All smithy simple shapes
21const SIMPLE_SHAPES: &[&str] = &[
22    "string",
23    "integer",
24    "long",
25    "blob",
26    "boolean",
27    "byte",
28    "double",
29    "float",
30    "short",
31    "bigDecimal",
32    "bigInteger",
33    "timestamp",
34    "document",
35];
36
37/// simple shapes + list, map, union
38const BASIC_TYPES: &[&str] = &[
39    "string",
40    "integer",
41    "long",
42    "blob",
43    "boolean",
44    "byte",
45    "double",
46    "float",
47    "short",
48    "list",
49    "map",
50    "union",
51    "bigDecimal",
52    "bigInteger",
53    "timestamp",
54    "document",
55];
56
57/// Pairing of template name and contents
58///
59pub type Template<'template> = (&'template str, &'template str);
60
61#[derive(Default, Debug)]
62pub struct RenderConfig<'render> {
63    /// Templates to be loaded for renderer. List of template name, data
64    pub templates: Vec<Template<'render>>,
65    /// Whether parser is in strict mode:
66    ///   If true, a variable used in template that is undefined would raise an error
67    ///   if false, an undefined variable would evaluate to 'falsey'
68    pub strict_mode: bool,
69}
70
71/// HBTemplate processor for code generation
72pub struct Renderer<'gen> {
73    /// Handlebars processor
74    hb: Handlebars<'gen>,
75}
76
77impl<'gen> Default for Renderer<'gen> {
78    fn default() -> Self {
79        // unwrap ok because only error condition occurs with templates, and default has none.
80        Self::init(&RenderConfig::default()).unwrap()
81    }
82}
83
84impl<'gen> Renderer<'gen> {
85    /// Initialize handlebars template processor.
86    pub fn init(config: &RenderConfig) -> Result<Self, crate::Error> {
87        let mut hb = Handlebars::new();
88        // don't use strict mode because
89        // it's easier in templates to use if we allow undefined ~= false-y
90        hb.set_strict_mode(config.strict_mode);
91        hb.register_escape_fn(handlebars::no_escape); //html escaping is the default and cause issue0
92
93        // add common helpers and templates
94        add_base_helpers(&mut hb);
95        for t in &config.templates {
96            hb.register_template_string(t.0, t.1)?;
97        }
98
99        Ok(Self { hb })
100    }
101
102    /// Adds template to internal dictionary
103    pub fn add_template(&mut self, template: Template) -> Result<(), crate::Error> {
104        self.hb.register_template_string(template.0, template.1)?;
105        Ok(())
106    }
107
108    /// render a template without registering it
109    pub fn render_template<T>(&self, template: &str, data: &T) -> Result<String, crate::Error>
110    where
111        T: Serialize,
112    {
113        let rendered = self.hb.render_template(template, data)?;
114        Ok(rendered)
115    }
116
117    /// Render a named template
118    pub fn render<T, W>(
119        &self,
120        template_name: &str,
121        data: &T,
122        writer: &mut W,
123    ) -> Result<(), crate::Error>
124    where
125        T: Serialize,
126        W: std::io::Write,
127    {
128        self.hb.render_to_write(template_name, data, writer)?;
129        Ok(())
130    }
131}
132
133fn arg_as_string<'reg, 'rc>(
134    h: &'reg Helper<'reg, 'rc>,
135    n: usize,
136    tag: &str,
137) -> Result<&'rc str, RenderError> {
138    // get first arg as string
139    h.param(n)
140        .ok_or_else(|| RenderError::new(format!("missing string param after {tag}")))?
141        .value()
142        .as_str()
143        .ok_or_else(|| {
144            RenderError::new(format!(
145                "{} expects string param, not {:?}",
146                tag,
147                h.param(n).unwrap().value()
148            ))
149        })
150}
151
152fn arg_as_obj<'reg, 'rc>(
153    h: &'reg Helper<'reg, 'rc>,
154    n: usize,
155    tag: &str,
156) -> Result<&'rc serde_json::Map<String, serde_json::Value>, RenderError> {
157    // get first arg as string
158    h.param(n)
159        .ok_or_else(|| RenderError::new(format!("missing object param after {tag}")))?
160        .value()
161        .as_object()
162        .ok_or_else(|| {
163            RenderError::new(format!(
164                "{} expects object param, not {:?}",
165                tag,
166                h.param(n).unwrap().value()
167            ))
168        })
169}
170
171fn arg_as_array<'reg, 'rc>(
172    h: &'reg Helper<'reg, 'rc>,
173    n: usize,
174    tag: &str,
175) -> Result<&'rc Vec<serde_json::Value>, RenderError> {
176    // get first arg as string
177    h.param(n)
178        .ok_or_else(|| RenderError::new(format!("missing array param after {tag}")))?
179        .value()
180        .as_array()
181        .ok_or_else(|| {
182            RenderError::new(format!(
183                "{} expects array param, not {:?}",
184                tag,
185                h.param(n).unwrap().value()
186            ))
187        })
188}
189
190#[derive(Clone, Copy)]
191struct ShapeHelper {}
192
193/// Convert map iterator into Vec of sorted shapes, adding the map's key as field _key to each item
194fn to_sorted_array<S: AsRef<str>>(mut shapes: Vec<(S, &Value)>) -> JsonValue {
195    // case-insensitive, numeric-aware sort
196    shapes.sort_unstable_by(|a, b| {
197        lexical_sort::natural_lexical_only_alnum_cmp(a.0.as_ref(), b.0.as_ref())
198    });
199
200    let shapes = shapes
201        .into_iter()
202        .map(|(k, v)| (k.as_ref().to_string(), v.as_object().unwrap().clone()))
203        .map(|(k, mut v)| {
204            v.insert("_key".to_string(), serde_json::Value::String(k));
205            serde_json::Value::Object(v)
206        })
207        .collect::<Vec<Value>>();
208    Value::Array(shapes)
209}
210
211impl HelperDef for ShapeHelper {
212    fn call_inner<'reg: 'rc, 'rc>(
213        &self,
214        h: &Helper<'reg, 'rc>,
215        _reg: &'reg Handlebars<'reg>,
216        _ctx: &'rc Context,
217        _rc: &mut RenderContext<'reg, 'rc>,
218    ) -> Result<ScopedJson<'reg, 'rc>, RenderError> {
219        let shape_kind = arg_as_string(h, 0, "filter_shapes")?.to_string();
220        let arr = arg_as_array(h, 1, "filter_shapes")?;
221
222        // filter by shape
223        let shapes = arr
224            .iter ()
225            .filter(|v| {
226                matches!(v.get("type"), Some(serde_json::Value::String(kind))
227                    if (&shape_kind == "simple" && SIMPLE_SHAPES.contains(&kind.as_str()) && !val_is_trait(v))
228                     || (&shape_kind == "types" && BASIC_TYPES.contains(&kind.as_str()) && !val_is_trait(v))
229                        || (&shape_kind == "trait" && val_is_trait(v))
230                        || (&shape_kind != "trait" && &shape_kind == kind && !val_is_trait(v))
231                )
232            })
233            .cloned()
234            .collect::<Vec<Value>>();
235        Ok(ScopedJson::Derived(Value::Array(shapes)))
236    }
237}
238
239#[derive(Clone, Copy)]
240struct NamespaceHelper {}
241
242impl HelperDef for NamespaceHelper {
243    fn call_inner<'reg: 'rc, 'rc>(
244        &self,
245        h: &Helper<'reg, 'rc>,
246        _reg: &'reg Handlebars<'reg>,
247        _ctx: &'rc Context,
248        _rc: &mut RenderContext<'reg, 'rc>,
249    ) -> Result<ScopedJson<'reg, 'rc>, RenderError> {
250        let namespace = arg_as_string(h, 0, "filter_namespace")?;
251        let namespace = NamespaceID::from_str(namespace)
252            .map_err(|e| RenderError::new(format!("invalid namespace {e}")))?;
253        let obj = arg_as_obj(h, 1, "filter_namespace")?;
254
255        let shapes = obj
256            .iter()
257            .filter_map(|(k, v)| match ShapeID::from_str(k) {
258                Ok(id) => Some((id, v)),
259                _ => None,
260            })
261            .filter(|(id, _)| id.namespace() == &namespace)
262            .map(|(id, v)| (id.to_string(), v))
263            .collect::<Vec<(String, &Value)>>();
264        Ok(ScopedJson::Derived(to_sorted_array(shapes)))
265    }
266}
267
268#[derive(Clone, Copy)]
269struct SimpleTypeHelper {}
270
271impl HelperDef for SimpleTypeHelper {
272    fn call_inner<'reg: 'rc, 'rc>(
273        &self,
274        h: &Helper<'reg, 'rc>,
275        _reg: &'reg Handlebars<'reg>,
276        _ctx: &'rc Context,
277        _rc: &mut RenderContext<'reg, 'rc>,
278    ) -> Result<ScopedJson<'reg, 'rc>, RenderError> {
279        let type_name = arg_as_string(h, 0, "is_simple")?;
280        Ok(ScopedJson::Derived(serde_json::Value::Bool(
281            SIMPLE_SHAPES.contains(&type_name),
282        )))
283    }
284}
285
286#[derive(Clone, Copy)]
287struct DocHelper {}
288
289impl HelperDef for DocHelper {
290    fn call_inner<'reg: 'rc, 'rc>(
291        &self,
292        h: &Helper<'reg, 'rc>,
293        _reg: &'reg Handlebars<'reg>,
294        _ctx: &'rc Context,
295        _rc: &mut RenderContext<'reg, 'rc>,
296    ) -> Result<ScopedJson<'reg, 'rc>, RenderError> {
297        let mut doc = String::new();
298        let shape_props = arg_as_obj(h, 0, "doc")?;
299        if let Some(JsonValue::Object(traits)) = shape_props.get("traits") {
300            if let Some(JsonValue::String(doc_value)) = traits.get(DOCUMENTATION_TRAIT) {
301                doc = doc_value.clone();
302                // TODO: should convert markdown to html!
303            }
304        }
305        Ok(ScopedJson::Derived(serde_json::Value::String(doc)))
306    }
307}
308
309/*
310#[derive(Clone, Copy)]
311struct TypeHelper {}
312
313/// pretty-print type names
314impl HelperDef for TypeHelper {
315    fn call_inner<'reg: 'rc, 'rc>(
316        &self,
317        h: &Helper<'reg, 'rc>,
318        _reg: &'reg Handlebars<'reg>,
319        _ctx: &'rc Context,
320        _rc: &mut RenderContext<'reg, 'rc>,
321    ) -> Result<Option<ScopedJson<'reg, 'rc>>, RenderError> {
322        let typ = arg_as_string(h, 0, "typ")?;
323
324        // strip off smithy.api since it makes everything too verbose
325        // (unwrap here because (per smithy json-ast spec) model was created with all absolute shape ids
326        let sid = ShapeID::from_str(typ).unwrap();
327        if sid.namespace() == &NamespaceID::new_unchecked("smithy.api") {
328            return Ok(Some(ScopedJson::Derived(serde_json::Value::String(
329                sid.shape_name().to_string(),
330            ))));
331        }
332
333        // if a namespace param was provided, strip off that if this type is local to that namespace
334        if let Ok(ns) = arg_as_string(h, 1, "typ") {
335            if &sid.namespace().to_string() == ns {
336                return Ok(Some(ScopedJson::Derived(serde_json::Value::String(
337                    sid.shape_name().to_string(),
338                ))));
339            }
340        }
341
342        // otherwise return as-is
343        Ok(Some(ScopedJson::Derived(serde_json::Value::String(
344            typ.to_string(),
345        ))))
346    }
347}
348 */
349
350/// Returns true if the shape is a trait
351fn map_is_trait(shape: &JsonMap) -> bool {
352    if let Some(JsonValue::Object(traits)) = shape.get("traits") {
353        traits.get(TRAIT_TRAIT).is_some()
354    } else {
355        false
356    }
357}
358
359/// Returns true if the shape is a trait
360fn val_is_trait(shape: &JsonValue) -> bool {
361    if let Some(JsonValue::Object(traits)) = shape.get("traits") {
362        traits.get(TRAIT_TRAIT).is_some()
363    } else {
364        false
365    }
366}
367
368#[derive(Clone, Copy)]
369struct TraitsHelper {}
370
371/// Returns a copy of the shape's traits without documentation trait
372impl HelperDef for TraitsHelper {
373    fn call_inner<'reg: 'rc, 'rc>(
374        &self,
375        h: &Helper<'reg, 'rc>,
376        _reg: &'reg Handlebars<'reg>,
377        _ctx: &'rc Context,
378        _rc: &mut RenderContext<'reg, 'rc>,
379    ) -> Result<ScopedJson<'reg, 'rc>, RenderError> {
380        let mut traits_no_doc = JsonMap::new();
381
382        let shape_props = arg_as_obj(h, 0, "traits")?;
383        if let Some(JsonValue::Object(traits)) = shape_props.get("traits") {
384            for (k, v) in traits.iter() {
385                if k != DOCUMENTATION_TRAIT && k != TRAIT_TRAIT {
386                    traits_no_doc.insert(k.clone(), v.clone());
387                }
388            }
389        }
390        Ok(ScopedJson::Derived(serde_json::Value::Object(
391            traits_no_doc,
392        )))
393    }
394}
395
396/*
397fn to_href_link(id: &str) -> String {
398    let id =
399        ShapeID::from_str(id).map_err(|e| RenderError::new(&format!("invalid shape id {}", e)))?;
400    let ns = strings::to_camel_case(id.namespace().to_string());
401    format!(
402        "<a href=\"../{}.html#{}\">{}</a>",
403        ns,
404        id.shape_name().to_string(),
405        id
406    )
407}:w
408
409 */
410
411#[derive(Clone, Copy)]
412struct IsTraitHelper {}
413
414/// Returns true if the shape is a trait
415impl HelperDef for IsTraitHelper {
416    fn call_inner<'reg: 'rc, 'rc>(
417        &self,
418        h: &Helper<'reg, 'rc>,
419        _reg: &'reg Handlebars<'reg>,
420        _ctx: &'rc Context,
421        _rc: &mut RenderContext<'reg, 'rc>,
422    ) -> Result<ScopedJson<'reg, 'rc>, RenderError> {
423        let shape = arg_as_obj(h, 0, "is_trait")?;
424        Ok(ScopedJson::Derived(serde_json::Value::Bool(map_is_trait(
425            shape,
426        ))))
427    }
428}
429
430/// Add template helpers functions
431fn add_base_helpers(hb: &mut Handlebars) {
432    // "shapes" filters a shape list for the shape kind
433    //   `shapes kind`      - uses 'this' for the list of shapes; should be called inside an #each block
434    //   `shapes kind list` - uses the provided 'list' object, assumed to be a dict of shapes in json-ast format
435    hb.register_helper("filter_shapes", Box::new(ShapeHelper {}));
436
437    // "namespaces" filters a shape list for shapes in the namespace
438    //   `namespaces ns`      - finds shapes in `this` that are in namespace ns
439    //   `namespaces ns list` - finds shapes in `list` in namespace ns
440    hb.register_helper("filter_namespace", Box::new(NamespaceHelper {}));
441
442    // "is_simple" returns true if the type parameter is one of the simple types
443    hb.register_helper("is_simple", Box::new(SimpleTypeHelper {}));
444
445    // "doc" extracts documentation for the object (or item)
446    hb.register_helper("doc", Box::new(DocHelper {}));
447
448    // "traits" returns object's traits without documentation
449    hb.register_helper("traits", Box::new(TraitsHelper {}));
450
451    // "traits" returns object's traits without documentation
452    hb.register_helper("is_trait", Box::new(IsTraitHelper {}));
453
454    //
455    // extract the namespace part of a ShapeID
456    //
457    hb.register_helper(
458        "namespace_name",
459        Box::new(
460            |h: &Helper,
461             _r: &Handlebars,
462             _: &Context,
463             _rc: &mut RenderContext,
464             out: &mut dyn Output|
465             -> HelperResult {
466                // get first arg as string
467                let id = arg_as_string(h, 0, "namespace")?;
468                let id = ShapeID::from_str(id).map_err(|e| {
469                    RenderError::new(format!("invalid shape id {e} for namespace_name"))
470                })?;
471                out.write(&id.namespace().to_string())?;
472                Ok(())
473            },
474        ),
475    );
476
477    hb.register_helper(
478        "typ",
479        Box::new(
480            |h: &Helper,
481             _r: &Handlebars,
482             _: &Context,
483             _rc: &mut RenderContext,
484             out: &mut dyn Output|
485             -> HelperResult {
486                let typ = arg_as_string(h, 0, "typ")?;
487                let sid = ShapeID::from_str(typ).unwrap();
488                // (unwrap here ok because (per smithy json-ast spec) model was created with all absolute shape ids
489                let sid_ns = sid.namespace();
490
491                let link: String = if sid_ns == &NamespaceID::new_unchecked("smithy.api") {
492                    // If it's in smithy.api, just use shape name since smithy.api makes it too verbose
493                    sid.shape_name().to_string()
494                } else {
495                    match arg_as_string(h, 1, "typ") {
496                        // If it's local to this file (namespace matches namespace parameter), strip it off and use local href
497                        Ok(ns) if sid_ns.to_string() == ns => {
498                            let id_shape = sid.shape_name().to_string();
499                            format!(
500                                "<a href=\"#{}\">{}</a>",
501                                &strings::to_snake_case(&id_shape),
502                                &id_shape,
503                            )
504                        }
505                        _ => format!(
506                            "<a href=\"./{}.html#{}\">{}</a>",
507                            &strings::to_snake_case(&sid_ns.to_string()),
508                            &strings::to_snake_case(&sid.shape_name().to_string()),
509                            sid
510                        ),
511                    }
512                };
513                out.write(&link)?;
514                Ok(())
515            },
516        ),
517    );
518
519    //
520    // extract the shape-name part of a ShapeID
521    //
522    hb.register_helper(
523        "shape_name",
524        Box::new(
525            |h: &Helper,
526             _r: &Handlebars,
527             _: &Context,
528             _rc: &mut RenderContext,
529             out: &mut dyn Output|
530             -> HelperResult {
531                let id = arg_as_string(h, 0, "shape_name")?;
532                let id = ShapeID::from_str(id).map_err(|e| {
533                    RenderError::new(format!("invalid shape id {e} for shape_name"))
534                })?;
535                out.write(&id.shape_name().to_string())?;
536                Ok(())
537            },
538        ),
539    );
540
541    //
542    // extract the member name of the shape, if any
543    //
544    hb.register_helper(
545        "member_name",
546        Box::new(
547            |h: &Helper,
548             _r: &Handlebars,
549             _: &Context,
550             _rc: &mut RenderContext,
551             out: &mut dyn Output|
552             -> HelperResult {
553                let id = arg_as_string(h, 0, "member_name")?;
554                let id = Identifier::from_str(id).map_err(|e| {
555                    RenderError::new(format!("invalid member id {e} for member_name"))
556                })?;
557                out.write(&id.to_string())?;
558                Ok(())
559            },
560        ),
561    );
562
563    //
564    // to_pascal_case
565    //
566    hb.register_helper(
567        "to_pascal_case",
568        Box::new(
569            |h: &Helper,
570             _r: &Handlebars,
571             _: &Context,
572             _rc: &mut RenderContext,
573             out: &mut dyn Output|
574             -> HelperResult {
575                let id = arg_as_string(h, 0, "to_pascal_case")?;
576                out.write(&strings::to_pascal_case(id))?;
577                Ok(())
578            },
579        ),
580    );
581
582    //
583    // to_snake_case
584    //
585    hb.register_helper(
586        "to_snake_case",
587        Box::new(
588            |h: &Helper,
589             _r: &Handlebars,
590             _: &Context,
591             _rc: &mut RenderContext,
592             out: &mut dyn Output|
593             -> HelperResult {
594                let id = arg_as_string(h, 0, "to_snake_case")?;
595                out.write(&strings::to_snake_case(id))?;
596                Ok(())
597            },
598        ),
599    );
600}