jamboree / bustache

C++20 implementation of {{ mustache }}
82 stars 10 forks source link

Is interface for Bustache C++20 incomplete? #25

Closed ElizabethMartinez2021 closed 2 years ago

ElizabethMartinez2021 commented 2 years ago

I am a Mustache for Javascript user and downloaded Bustache (C++20). I wanted to use the variant data type for Json values and had trouble.

After looking at issues, I noticed messages for Bustache (C++11) so I downloaded and easily used variant for Json values.

Ok, interface for C++11's bustache::object and bustache::array seem much easier to use than the example given for C++20.

My next step was to give Bustache C++20 another shot and included "../test/model.hpp", replaced namespace to test::object and test::array. I was so happy to see everything compile but then when I went to build, I got a cryptic "error LNK2005" about something already being defined.

My guess is that the test/model.hpp has not been finalized and that struct value; is currently a global causing the link error.

I don't have C++ knowledge to work around this linker error, does anyone have a revision available to test/model.hpp so that I can use variant value type?

jamboree commented 2 years ago

Is the link error you see related to this line? If so, try add inline in the front.

BTW, if you're working with JSON, you could see this example.

ElizabethMartinez2021 commented 2 years ago

Thank you for your comments. I don't have any problems compiling/linking either of the two demos provided BUT the linker error that I get happens after including the following line into multiple files within a project:

#include "../test/model.hpp" fatal error LNK1169: one or more multiply defined symbols found

Including the file above provides flexibility of working with a variant values, e.g.: bool, string, int, etc. just like the older C++11 Bustache versions allowed.

With the C++20, I can include the model.hpp above but then get linker errors when including it into multiple source files.

For me at least, it would be so nice if Bustache C++20 had variant built into the objects just like the older Bustache versions. Are you planning to extend the flexibility of the interface or is Bustache going in a new direction?

P.S. My interest in the C++20 version is being able to easily work with Nlohmann json, e.g. load any given JSON file into RAM and seamlessly pass it to a Bustache container where I can simply create the template.

Using the older Bustache version, I would have to manually code and load the Json into a Bustache objects/arrays and create the template. My guess is that the latest version of Bustache would load the data automatically without any c++ coding.

jamboree commented 2 years ago

include "../test/model.hpp"

This file is used for test internally, if you really want to use it, try the modification I mentioned earlier.

Including the file above provides flexibility of working with a variant values, e.g.: bool, string, int, etc. just like the older C++11 Bustache versions allowed.

You don't need that file to work with std::variant, it should work out-of-the-box.

My guess is that the latest version of Bustache would load the data automatically without any c++ coding.

The older design requires an intermediate data model; the new design is more like Rust's trait system so that it can work on the user data directly without dynamic allocation.

ElizabethMartinez2021 commented 2 years ago

I am able to use Bustache C++20 to give me accurate output like this:

void quick_example()
{
    typedef std::variant<std::string, int, double, bool> dataType;
    typedef std::vector<dataType> arrayType;
    typedef std::variant<dataType, arrayType> valueType;

    bustache::format tmpl{ "{{mustache}} templating. {{logic}}. {{percent}}%.  My array: {{#numbers}}{{.}} {{/numbers}}" };
    std::unordered_map<std::string, valueType> data{ {"mustache", "bustache"}, {"logic", true} };

    // insert double
    data.emplace("percent", 100.9999);

    // insert vector
    arrayType myArray{ 1, "hi", 5.25, false };
    data.emplace("numbers", myArray);

    // std::cout << tmpl(data) << "\n";

    std::string result = bustache::to_string(tmpl(data).escape(bustache::escape_html));
    std::cout << result << '\n';
}

Output: bustache templating. true. 100.9999%. My array: 1 hi 5.25 false

I had to read about Variant and the code above makes sense to me. Is this the way Bustache is supposed to be used?

I just could not figure out how to create an object for the 'value'. For example key: userId, value: object of many key value data pairs.

If you could guide me in the right direction, I would sincerely appreciate that, thank you.

jamboree commented 2 years ago

Like this?

#include <iostream>
#include <variant>
#include <vector>
#include <unordered_map>
#include <bustache/model.hpp>
#include <bustache/render/ostream.hpp>

namespace my {
    struct Value;

    struct Object : std::unordered_map<std::string, Value> {
        using unordered_map::unordered_map;
    };

    using Array = std::vector<Value>;

    struct Value : std::variant<bool, int, double, std::string, Object, Array> {
        using variant::variant;

        Value(char const* str) : variant(std::string(str)) {}
    };
} // namespace my

template<>
struct bustache::impl_compatible<my::Value> {
    static value_ptr get_value_ptr(my::Value const& self) {
        return std::visit([](auto const& val) { return value_ptr(&val); },
                          self);
    }
};

int main() {
    bustache::format tmpl{"{{mustache}} templating. {{logic}}. {{percent}}%.  "
                          "My array: {{#numbers}}{{.}} {{/numbers}}"};
    my::Object data{{"mustache", "bustache"}, {"logic", true}};

    // insert double
    data.emplace("percent", 100.9999);

    // insert vector
    my::Array myArray{1, "hi", 5.25, false};
    data.emplace("numbers", myArray);

    std::cout << tmpl(data).escape(bustache::escape_html) << "\n";
}

Note that I'm not sure if std::unordered_map allows incomplete value-type, but at least it seems to work on msvc/gcc/clang.

ElizabethMartinez2021 commented 2 years ago

Hi Jamboree, thank you! The code above is incredibly useful! I am now able to programmatically load a Bustache container with variant data as well as read a Json file from disk to accomplish the same thing. For now, this is working perfectly with VC++ 2019 and will check with GCC later today.

Here is an example how I am programmatically loading a Bustache container:

#include "quick_example3.h"
#include <bustache/render/string.hpp>
#include <nlohmann/json.hpp>
#include "my.h"

void quick_example3()
{
    std::cout << "-----------------------\nQUICK EXAMPLE #3\n";
    bustache::format tmpl{
        "{{mustache}} templating. {{logic}}. {{percent}}%.\n"
        "My array: {{#numbers}}{{.}} {{/numbers}}\n\n" 
        "My favorite titles:\n{{#books}}Title: {{title}}.  {{author}}, {{year}}.\n{{/books}}"
    };

    // bustache data object
    my::Object data{ {"mustache", "bustache"}, {"logic", true} };

    // insert double
    data.emplace("percent", 100.9999);

    // insert array
    my::Array myArray{ 1, "hi", 5.25, false };
    data.emplace("numbers", myArray);

    // insert objects into array
    my::Array booksArray;
    {
        my::Object book;
        book.emplace("title", "Animal Farm");
        book.emplace("author", "George Orwell");
        book.emplace("year", 1945);
        booksArray.emplace_back(book);
    }
    {
        my::Object book;
        book.emplace("title", "1984");
        book.emplace("author", "George Orwell");
        book.emplace("year", 1949);
        booksArray.emplace_back(book);
    }
    {
        my::Object book;
        book.emplace("title", "2000 Mules");
        book.emplace("author", "Dinesh D'Souza");
        book.emplace("year", 2022);
        booksArray.emplace_back(book);
    }
    {
        my::Object book;
        book.emplace("title", "Endgame: Blueprint for global enslavement");
        book.emplace("author", "Alex Jones");
        book.emplace("year", 2007);
        booksArray.emplace_back(book);
    }

    // insert array of objects
    data.emplace("books", booksArray);

    std::cout << tmpl(data).escape(bustache::escape_html) << "\n";
}
ElizabethMartinez2021 commented 2 years ago

Here is an example how I am loading a Bustache container from Json:

books.json

{
    "mustache": "BUSTACHE",
    "logic": true,
    "percent": 777.77777,
    "numbers": [1, "HI", 5.25, true],
    "books":
    [
        {
            "title": "ANIMAL FARM",
            "author": "GEORGE ORWELL",
            "year": 1945
        },
        {
            "title": "1984",
            "author": "GEORGE ORWELL",
            "year": 1949
        },
        {
            "title": "2000 MULES",
            "author": "DINESH D'SOUZA",
            "year": 2022
        },
        {
            "title": "ENDGAME: BLUEPRINT FOR GLOBAL ENSLAVEMENT",
            "author": "ALEX JONES",
            "year": 2007
        }
    ]
}

and the code:

void quick_example4()
{
    try
    {
        std::cout << "-----------------------\nQUICK EXAMPLE #4\n";
        bustache::format tmpl{
            "{{mustache}} templating. {{logic}}. {{percent}}%.\n"
            "My array: {{#numbers}}{{.}} {{/numbers}}\n\n"
            "My favorite titles:\n"
            "{{#books}}Title: {{title}}.  {{author}}, {{year}}.\n{{/books}}"
        };

        auto const json = nlohmann::json::parse(read_file("../../books.json"));
        std::cout << tmpl(json).escape(bustache::escape_html);
    }
    catch (const std::exception& e)
    {
        std::cerr << e.what();
    }
}
ElizabethMartinez2021 commented 2 years ago

What I did was take the code you posted above and some from your nlohmann demo into a header file that can be included into any required source files:

my.h

#ifndef _MY_H_
#define _MY_H_
#include <variant>
#include <unordered_map>
#include <bustache/model.hpp>
#include <bustache/render/ostream.hpp>
#include <nlohmann/json.hpp>

// IMPORTANT: the code below was given to my by JAMBOREE.  Including this into your user code
// provided me with a great deal of flexibility, mainly the ability to easily work with variant 
// values in my bustache data containers.

namespace my {
    struct Value;

    struct Object : std::unordered_map<std::string, Value> {
        using unordered_map::unordered_map;
    };

    using Array = std::vector<Value>;

    struct Value : std::variant<bool, int, double, std::string, Object, Array> {
        using variant::variant;

        Value(char const* str) : variant(std::string(str)) {}
    };
} // namespace my

template<>
struct bustache::impl_compatible<my::Value> {
    static value_ptr get_value_ptr(my::Value const& self) {
        return std::visit([](auto const& val) { return value_ptr(&val); },
            self);
    }
};

// IMPORTANT: the code copied below comes from the nlohmann.cpp Bustache demo.

// Typically, you don't need to explicitly delete the impl_model, but nlohmann::json
// is basically a amoeba that prentends to be any model, and that will cause hard
// errors in concept checking due to recursion.
template<>
struct bustache::impl_model<nlohmann::json>
{
    impl_model() = delete;
};

template<>
struct bustache::impl_compatible<nlohmann::json>
{
    static value_ptr get_value_ptr(nlohmann::json const& self)
    {
        nlohmann::json::value_t const kind(self);
        switch (kind)
        {
        case nlohmann::json::value_t::boolean:
            return value_ptr(self.get_ptr<nlohmann::json::boolean_t const*>());
        case nlohmann::json::value_t::number_integer:
            return value_ptr(self.get_ptr<nlohmann::json::number_integer_t const*>());
        case nlohmann::json::value_t::number_unsigned:
            return value_ptr(self.get_ptr<nlohmann::json::number_unsigned_t const*>());
        case nlohmann::json::value_t::number_float:
            return value_ptr(self.get_ptr<nlohmann::json::number_float_t const*>());
        case nlohmann::json::value_t::string:
            return value_ptr(self.get_ptr<nlohmann::json::string_t const*>());
        case nlohmann::json::value_t::array:
            return value_ptr(self.get_ptr<nlohmann::json::array_t const*>());
        case nlohmann::json::value_t::object:
            return value_ptr(self.get_ptr<nlohmann::json::object_t const*>());
        }
        return value_ptr();
    }
};

#endif

Thank you once again for such an incredible library.

ElizabethMartinez2021 commented 2 years ago

One final question that I am curious about, when loading a nlohmann json object and passing it to the template, does the internal code copy or move the data into a Bustache data object format?

I looked at impl_compatible but to be quite honest, that code is beyond my C++ capacity.

P.S. at this time I can use either a Bustache or Nlohmann data object as a data source for the template. I know that a performance difference would be almost immeasurable, especially so for my use case but I do wonder if copy or move occurs. Thank you.

jamboree commented 2 years ago

Nothing is copied. Everything is used as-is. The trait system tells Bustache how to treat the data.