telemachus 0.1.0

Another template library, suporting serialization
Documentation
use std::borrow::Cow;
use std::collections::BTreeMap;

use crate::{Block, SelPart, Selector, Template};

use eyre::{bail, eyre, Result, WrapErr};
use serde::Serialize;
use serde_json::{to_value, Value};

// TODO: Dont use eyre, have a dedicated type

struct Scope {
    content: Value,
    more: BTreeMap<String, Value>,
}

fn number_non_zero(n: &serde_json::Number) -> bool {
    if let Some(n) = n.as_f64() {
        n != 0.0
    } else if let Some(n) = n.as_i64() {
        n != 0
    } else if let Some(n) = n.as_u64() {
        n != 0
    } else {
        panic!("Funky number {}", n)
    }
}

impl Scope {
    fn get(&self, sel: &Selector) -> Result<&Value> {
        let mut sels = &sel.items[..];

        if sel.items.is_empty() {
            bail!("Empty selector {:?}", sel);
        }

        let mut place = match &sel.items[0] {
            SelPart::Map(key) => {
                let k: &str = &key;
                if let Some(place) = self.more.get(k) {
                    // We've mathced on the first item to get place,
                    // so remove it from the queue.
                    sels = &sels[1..];
                    place
                } else {
                    &self.content
                }
            }
            SelPart::Array(_) => &self.content,
        };

        for i in sels {
            match i {
                SelPart::Map(k) => {
                    let k: &str = &k; // Type help for rustc
                    place = place
                        .as_object()
                        .ok_or_else(|| eyre!("Expected object"))?
                        .get(k)
                        .ok_or_else(|| eyre!("Key not found"))?;
                }
                SelPart::Array(idx) => {
                    place = place
                        .as_array()
                        .ok_or_else(|| eyre!("Expected array"))?
                        .get(*idx)
                        .ok_or_else(|| eyre!("OOB"))?;
                }
            }
        }
        Ok(place)
    }

    fn get_to_str(&self, sel: &Selector) -> Result<Cow<'_, str>> {
        let place = self.get(sel)?;

        Ok(match place {
            Value::Null => Cow::Borrowed(""),
            Value::Bool(n) => Cow::Borrowed(if *n { "true" } else { "false" }),
            Value::Number(n) => Cow::Owned(n.to_string()),
            Value::String(s) => Cow::Borrowed(&s),
            Value::Array(_) => {
                bail!("Got array, expected null|bool|number|string")
            }
            Value::Object(_) => {
                bail!("Got object, expected null|bool|number|string")
            }
        })
    }

    fn is_truthy(&self, sel: &Selector) -> bool {
        self.get(sel)
            .map(|x| match x {
                Value::Null => false,
                Value::Bool(b) => *b,
                Value::Number(n) => number_non_zero(n),
                Value::String(s) => !s.is_empty(),
                Value::Array(a) => !a.is_empty(),
                Value::Object(o) => !o.is_empty(),
            })
            .unwrap_or(false)
    }

    fn assert_not_in(&self, key: &str) -> Result<()> {
        if let Value::Object(o) = &self.content {
            if o.contains_key(key) {
                bail!("Found {}, expected none", key);
            }
        }

        if self.more.contains_key(key) {
            bail!("Found {}, expected none", key);
        }

        Ok(())
    }

    fn set_extra(&mut self, key: &str, value: serde_json::Value) {
        // If it's already in the map, reuse the string
        if let Some(ptr) = self.more.get_mut(key) {
            *ptr = value;
        } else {
            self.more.insert(key.to_owned(), value);
        }
    }

    fn remove_extra(&mut self, key: &str) {
        self.more.remove(key);
    }
}

fn render_in(parts: &[Block<'_>], scope: &mut Scope, str: &mut String) -> Result<()> {
    use Block::*;

    for i in parts {
        match i {
            HtmlLit(s) => str.push_str(&s),
            PutUnescaped(sel) => {
                let s = scope.get_to_str(sel)?;
                str.push_str(&s);
            }
            PutEscaped(sel) => {
                let s = scope.get_to_str(sel)?;
                // https://docs.rs/tera/1.6.1/src/tera/utils.rs.html#18-34
                // TODO: be faster
                for c in s.chars() {
                    match c {
                        '&' => str.push_str("&amp;"),
                        '<' => str.push_str("&lt;"),
                        '>' => str.push_str("&gt;"),
                        '"' => str.push_str("&quot;"),
                        '\'' => str.push_str("&#x27;"),
                        '/' => str.push_str("&#x2F;"),
                        _ => str.push(c),
                    }
                }
            }

            If {
                sel,
                if_true,
                if_false,
            } => {
                let add = if scope.is_truthy(sel) {
                    if_true
                } else {
                    if_false
                };

                render_in(add, scope, str)?;
            }
            For {
                name,
                iter_over,
                each,
            } => {
                let vals = scope.get(iter_over)?;
                scope.assert_not_in(name)?;
                let vals = match vals {
                    // TODO: dont clone, but it's fun borrow checking
                    Value::Array(a) => a.clone(),
                    other => bail!("Expected array, got {}", other),
                };

                for v in vals {
                    scope.set_extra(name, v);
                    render_in(each, scope, str)?;
                }
                scope.remove_extra(name);
            }
            Comment => {}
        }
    }

    Ok(())
}

impl<'t> Template<'t> {
    pub fn render<T: Serialize>(&self, content: T) -> Result<String> {
        let content = to_value(content).context("Couldnt convert `content` to json value")?;
        let mut scope = Scope {
            content,
            more: BTreeMap::new(),
        };

        // TODO: Figure out a good value for this
        // for now, assume every page is 20kb
        let mut str = String::with_capacity(20_000);

        render_in(&self.parts, &mut scope, &mut str)?;

        Ok(str)
    }
}

#[cfg(test)]
mod tests {
    use crate::*;
    use render::Scope;
    use serde_json::json;
    use smallvec::smallvec;

    // TODO: Arrays and nested.

    #[test]
    fn basic_tests() {
        let mut tpl = Template {
            parts: vec![
                Block::HtmlLit(Cow::Borrowed("Start\n")), // Block::PutUnescaped()
            ],
        };

        assert_eq!(tpl.render(json!({})).unwrap(), "Start\n");

        tpl.parts.push(Block::PutEscaped(Selector {
            items: smallvec![SelPart::Map(Cow::Borrowed("a"))],
        }));

        assert!(tpl.render(json!({})).is_err());

        assert_eq!(
            tpl.render(json!({ "a": "Hello\n" })).unwrap(),
            "Start\nHello\n"
        );

        assert_eq!(tpl.render(json!({ "a": 32 })).unwrap(), "Start\n32");

        assert!(tpl.render(json!({ "a": vec![1, 2, 3] })).is_err());

        assert_eq!(
            tpl.render(json!({ "a": Option::<()>::None })).unwrap(),
            "Start\n"
        );

        assert_eq!(
            tpl.render(json!({ "a": "<hello>" })).unwrap(),
            "Start\n&lt;hello&gt;"
        );

        tpl.parts.push(Block::PutUnescaped(Selector {
            items: smallvec![SelPart::Map(Cow::Borrowed("b"))],
        }));

        assert!(tpl.render(json!({ "a": "Hello\n" })).is_err());

        assert_eq!(
            tpl.render(json!({
                "a": "<hello>",
                "b": "<hello>",
            }))
            .unwrap(),
            "Start\n&lt;hello&gt;<hello>"
        );
    }

    #[test]
    fn nested() {
        let tpl = Template {
            parts: vec![
                Block::PutUnescaped(Selector {
                    items: smallvec![SelPart::Map(Cow::Borrowed("b"))],
                }),
                Block::HtmlLit(Cow::Borrowed("*")),
                Block::PutUnescaped(Selector {
                    items: smallvec![SelPart::Map(Cow::Borrowed("a"))],
                }),
                Block::HtmlLit(Cow::Borrowed("*")),
                Block::PutUnescaped(Selector {
                    items: smallvec![
                        SelPart::Map(Cow::Borrowed("c")),
                        SelPart::Map(Cow::Borrowed("d"))
                    ],
                }),
            ],
        };

        assert_eq!(
            tpl.render(json!({
                "b": "B",
                "a": "A",
                "c": {
                    "d": "D"
                }
            }))
            .unwrap(),
            "B*A*D"
        );

        assert_eq!(
            tpl.render(json!({
                "b": "B",
                "d": ["1","2","3"],
                "c": {
                    "d": "D",
                    "e": {
                        "f": "j"
                    }
                },
                "a": 1,

            }))
            .unwrap(),
            "B*1*D"
        );
    }

    #[test]
    fn arrays_basic() {
        let tpl = Template {
            parts: vec![Block::PutUnescaped(Selector {
                items: smallvec![SelPart::Array(1)],
            })],
        };

        assert_eq!(tpl.render(json!(["abc", "def", "ghi"])).unwrap(), "def");

        assert!(tpl.render(json!(["d"])).is_err());

        assert!(tpl.render(json!(["x", [1, 2], "y"])).is_err());
    }

    #[test]
    fn arrays_nested() {
        let tpl = Template {
            parts: vec![Block::PutUnescaped(Selector {
                items: smallvec![
                    SelPart::Array(3),
                    SelPart::Map(Cow::Borrowed("0")),
                    SelPart::Array(2)
                ],
            })],
        };

        assert_eq!(
            tpl.render(json!([
                {},
                {},
                {},
                {
                    "x": "fd",
                    "g": "fds",
                    "0": ["d", "d", "ghj"]
                }
            ]))
            .unwrap(),
            "ghj"
        );

        assert!(tpl.render(json!({})).is_err());
        assert!(tpl.render(json!({"a": 1, "b": 2})).is_err());

        assert!(tpl.render(json!([{}, {}, {}, {}, {},])).is_err());

        assert!(tpl
            .render(json!([
                0,
                0,
                0,
                {
                    "0": "0"
                }
            ]))
            .is_err());

        assert!(tpl
            .render(json!([
                0,0,0,
                {
                    "0": [0,1]
                }
            ]))
            .is_err());

        assert_eq!(
            tpl.render(json!([
                0,0,0,
                {
                    "0": [0,1,2]
                }
            ]))
            .unwrap(),
            "2"
        );

        assert!(tpl
            .render(json!([
                0,0,0,
                {
                    "0": [0,1,[1,2]]
                }
            ]))
            .is_err(),);
    }

    #[test]
    fn sel_get() {
        let scope = Scope {
            content: json!({
                "a": "b",
                "c": "d",
            }),
            more: maplit::btreemap! {
                "i".to_owned() => json!({"j": "k"})
            },
        };

        assert_eq!(
            scope
                .get(&Selector {
                    items: smallvec![SelPart::Map(Cow::Borrowed("a"))],
                })
                .unwrap(),
            &json!("b"),
        );

        assert_eq!(
            scope
                .get(&Selector {
                    items: smallvec![SelPart::Map(Cow::Borrowed("i"))],
                })
                .unwrap(),
            &json!({"j": "k"}),
        );

        assert!(scope
            .get(&Selector {
                items: smallvec![SelPart::Map(Cow::Borrowed("j"))]
            })
            .is_err());

        assert!(scope
            .get(&Selector {
                items: smallvec![SelPart::Array(0)]
            })
            .is_err())
    }

    #[test]
    fn sel_get_array_root() {
        let scope = Scope {
            content: json!([
                "0",
                {
                    "b": "c",
                    "d": [1,2],
                },
                1,
            ]),
            more: maplit::btreemap! {
                "i".to_owned() => json!({}),
                "j".to_owned() => json!({
                    "a": "c",
                    "e": [111,8,1]
                })
            },
        };

        assert_eq!(
            scope
                .get(&Selector {
                    items: smallvec![SelPart::Array(0)]
                })
                .unwrap(),
            &json!("0")
        );

        assert_eq!(
            scope
                .get(&Selector {
                    items: smallvec![SelPart::Array(1), SelPart::Map(Cow::Borrowed("d"))]
                })
                .unwrap(),
            &json!([1, 2])
        );

        assert_eq!(
            scope
                .get(&Selector {
                    items: smallvec![SelPart::Map(Cow::Borrowed("i"))],
                })
                .unwrap(),
            &json!({}),
        );

        assert_eq!(
            scope
                .get(&Selector {
                    items: smallvec![
                        SelPart::Map(Cow::Borrowed("j")),
                        SelPart::Map(Cow::Borrowed("e")),
                        SelPart::Array(1)
                    ],
                })
                .unwrap(),
            &json!(8),
        );

        assert!(scope
            .get(&Selector {
                items: smallvec![SelPart::Array(3)]
            })
            .is_err());
    }

    #[test]
    fn for_loop() {
        let tpl = Template {
            parts: vec![
                Block::HtmlLit(Cow::Borrowed("<ul>")),
                Block::For {
                    name: Cow::Borrowed("i"),
                    iter_over: Selector {
                        items: smallvec![SelPart::Map(Cow::Borrowed("items"))],
                    },
                    each: vec![
                        Block::HtmlLit(Cow::Borrowed("<li>")),
                        Block::PutEscaped(Selector {
                            items: smallvec![SelPart::Map(Cow::Borrowed("i"))],
                        }),
                        Block::HtmlLit(Cow::Borrowed("</li>")),
                    ],
                },
                Block::HtmlLit(Cow::Borrowed("</ul>")),
            ],
        };

        assert!(tpl.render(json!({})).is_err());
        assert!(tpl.render(json!({"items": "hello"})).is_err());
        assert!(tpl.render(json!({"items": [1,2,3, {"x": "y"}]})).is_err());

        assert_eq!(
            tpl.render(json!({"items": ["one", "two", "three"]}))
                .unwrap(),
            "<ul><li>one</li><li>two</li><li>three</li></ul>"
        );

        assert_eq!(
            tpl.render(json!(
            {
                "items": ["one", "two", "three"],
                "more-items": [5,3,1]
            }))
            .unwrap(),
            "<ul><li>one</li><li>two</li><li>three</li></ul>"
        );

        assert_eq!(
            tpl.render(json!(
            {
                "items": ["one", "two", "three"],
                "more-items": [5,3,1]
            }))
            .unwrap(),
            "<ul><li>one</li><li>two</li><li>three</li></ul>"
        );
    }

    #[test]
    fn for_if() {
        let tpl1 = Template {
            parts: vec![
                Block::HtmlLit(Cow::Borrowed("<ul>")),
                Block::For {
                    name: Cow::Borrowed("i"),
                    iter_over: Selector {
                        items: smallvec![SelPart::Map(Cow::Borrowed("items"))],
                    },
                    each: vec![
                        Block::HtmlLit(Cow::Borrowed("<li>")),
                        Block::If {
                            sel: Selector {
                                items: smallvec![
                                    SelPart::Map(Cow::Borrowed("i")),
                                    SelPart::Map(Cow::Borrowed("bold"))
                                ],
                            },
                            if_true: vec![Block::HtmlLit(Cow::Borrowed("<b>"))],
                            if_false: vec![],
                        },
                        Block::PutEscaped(Selector {
                            items: smallvec![
                                SelPart::Map(Cow::Borrowed("i")),
                                SelPart::Map(Cow::Borrowed("name"))
                            ],
                        }),
                        Block::If {
                            sel: Selector {
                                items: smallvec![
                                    SelPart::Map(Cow::Borrowed("i")),
                                    SelPart::Map(Cow::Borrowed("bold"))
                                ],
                            },
                            if_true: vec![Block::HtmlLit(Cow::Borrowed("</b>"))],
                            if_false: vec![],
                        },
                        Block::HtmlLit(Cow::Borrowed("</li>")),
                    ],
                },
                Block::HtmlLit(Cow::Borrowed("</ul>")),
            ],
        };

        let tpl2 = Template {
            parts: vec![
                Block::HtmlLit(Cow::Borrowed("<ul>")),
                Block::For {
                    name: Cow::Borrowed("i"),
                    iter_over: Selector {
                        items: smallvec![SelPart::Map(Cow::Borrowed("items"))],
                    },
                    each: vec![
                        Block::HtmlLit(Cow::Borrowed("<li>")),
                        Block::If {
                            sel: Selector {
                                items: smallvec![
                                    SelPart::Map(Cow::Borrowed("i")),
                                    SelPart::Map(Cow::Borrowed("bold"))
                                ],
                            },
                            if_true: vec![
                                Block::HtmlLit(Cow::Borrowed("<b>")),
                                Block::PutEscaped(Selector {
                                    items: smallvec![
                                        SelPart::Map(Cow::Borrowed("i")),
                                        SelPart::Map(Cow::Borrowed("name"))
                                    ],
                                }),
                                Block::HtmlLit(Cow::Borrowed("</b>")),
                            ],
                            if_false: vec![Block::PutEscaped(Selector {
                                items: smallvec![
                                    SelPart::Map(Cow::Borrowed("i")),
                                    SelPart::Map(Cow::Borrowed("name"))
                                ],
                            })],
                        },
                        Block::HtmlLit(Cow::Borrowed("</li>")),
                    ],
                },
                Block::HtmlLit(Cow::Borrowed("</ul>")),
            ],
        };

        for tpl in &[tpl1, tpl2] {
            assert!(tpl.render(json!({})).is_err());
            assert!(tpl.render(json!({"items": "hello"})).is_err());
            assert!(tpl.render(json!({"items": [1,2,3, {"x": "y"}]})).is_err());
            assert!(tpl
                .render(json!({"items": ["one", "two", "three"]}))
                .is_err());

            assert_eq!(
                tpl.render(json!({"items": [
                    { "name": "one", "bold": false},
                    { "name": "two", "bold": false},
                    { "name": "three", "bold": false},

                ]}))
                .unwrap(),
                "<ul><li>one</li><li>two</li><li>three</li></ul>"
            );

            assert_eq!(
                tpl.render(json!({"items": [
                    { "name": "one", "bold": true},
                    { "name": "two", "bold": false},
                    { "name": "three", "bold": true},

                ]}))
                .unwrap(),
                "<ul><li><b>one</b></li><li>two</li><li><b>three</b></li></ul>"
            );

            // Convert to bool false
            assert_eq!(
                tpl.render(json!({"items": [
                    { "name": "one", "bold": {}},
                    { "name": "two", "bold": 0},
                    { "name": "three", "bold": []},
                    { "name": "four", "bold": ""},
                    { "name": "five", "bold": Option::<()>::None }

                ]}))
                .unwrap(),
                "<ul><li>one</li><li>two</li><li>three</li><li>four</li><li>five</li></ul>"
            );

            assert_eq!(
            tpl.render(json!({"items": [
                { "name": "one", "bold": {"a": "one"}},
                { "name": "two", "bold": 32},
                { "name": "three", "bold": [{}, {}]},
                { "name": "four", "bold": "   "},
                { "name": "five", "bold": -2.3 }

            ]}))
            .unwrap(),
            "<ul><li><b>one</b></li><li><b>two</b></li><li><b>three</b></li><li><b>four</b></li><li><b>five</b></li></ul>"
        );
        }
    }
}