Search code examples
c++jsonboostboost-spirit

Boost Karma generator for composition of classes


I've the following class diagram:

class diagram

There's some unused class like BinaryOperator, but my real code needs them so I want to keep them also in the example.

I want to use boost::karma in order to obtain a JSON representation of this. The JSON should be like the following one:

{
  "name": "Plus",
  "type": "Function",
  "arguments": [
    {
      "name": "IntegerValue",
      "type": "Value",
      "value": "4"
    },
    {
      "name": "Plus",
      "type": "Function",
      "arguments": [
        {
          "name": "IntegerValue",
          "type": "Value",
          "value": "5"
        },
        {
          "name": "IntegerValue",
          "type": "Value",
          "value": "6"
        }
      ]
    }
  ]
}

Since it's a simple example, I'd like to use BOOST_FUSION_ADAPT_ADT macro for my classes in order to modularize the generator.

I'm new to Karma, I've read the tutorial on boost site but I don't understand how to attack my problem. I can't find some good tutorial about that macro.

I don't want to use existing libraries for JSON because at first I want to learn Karma, and in second place JSON is only an example, I need to export my expression in many formats, and I can do it by simply changing generators while the code that uses BOOST_FUSION_ADAPT_ADT for my classes should be the same.

You can find the code for creating a sample expression. Where I need to start in order to solve my problem?

#include <boost/lexical_cast.hpp>
#include <iostream>
#include <vector>

class Expression {
public:

  virtual std::string getName() const = 0;
};

class Value : public Expression {
public:

  virtual std::string getValue() const = 0;
};

class IntegerValue : public Value {
public:

  IntegerValue(int value) : m_value(value) {}
  virtual std::string getName() const override { return "IntegerValue"; }
  virtual std::string getValue() const override { return boost::lexical_cast<std::string>(m_value); }

private:

  int m_value;
};

class Function : public Expression {
public:

  void addArgument(Expression* expression) { m_arguments.push_back(expression); }
  virtual std::string getName() const override { return m_name; }

protected:

  std::vector<Expression*> m_arguments;
  std::string m_name;
};

class Plus : public Function {
public:

  Plus() : Function() { m_name = "Plus"; }
};

///////////////////////////////////////////////////////////////////////////////

int main(int argc, char **argv) {

  // Build expression 4 + 5 + 6 as 4 + (5 + 6)
  Function* plus1 = new Plus();
  Function* plus2 = new Plus();
  Value* iv4   = new IntegerValue(4);
  Value* iv5   = new IntegerValue(5);
  Value* iv6   = new IntegerValue(6);
  plus2->addArgument(iv5);
  plus2->addArgument(iv6);
  plus1->addArgument(iv4);
  plus1->addArgument(plus2);

  // Generate json string here, but how?

  return 0;
}

Solution

  • I'd advise against using Karma to generate JSON. I'd advise strongly against ADAPT_ADT (it's prone to very subtle UB bugs and it means you're trying to adapt something that wasn't designed for it. Just say no).

    Here's my take on it. Let's take the high road and be as unintrusive as possible. That means

    • We can't just overload operator<< to print json (because you may want to naturally print the expressions instead)
    • It also means that what ever function is responsible for generating the JSON doesn't

      • have to bother with json implementation details
      • have to bother with pretty formatting
    • Finally, I wouldn't want to intrude on the expression tree with anything JSON specific. The most that could be acceptable is an opaque friend declaration.


    A simple JSON facility:

    This might well be the most simplistic JSON representation, but it does the required subset and makes a number of smart choices (supporting duplicate properties, retaining property order for example):

    #include <boost/variant.hpp>
    namespace json {
        // adhoc JSON rep
        struct Null {};
        using String = std::string;
    
        using Value = boost::make_recursive_variant<
            Null,
            String,
            std::vector<boost::recursive_variant_>,
            std::vector<std::pair<String, boost::recursive_variant_> >
        >::type;
    
        using Property = std::pair<String, Value>;
        using Object = std::vector<Property>;
        using Array = std::vector<Value>;
    }
    

    That's all. This is fully functional. Let's prove it


    Pretty Printing JSON

    Like with the Expression tree itself, let's not hardwire this, but instead create a pretty-printing IO manipulator:

    #include <iomanip>
    namespace json {
    
        // pretty print it
        struct pretty_io {
            using result_type = void;
    
            template <typename Ref>
            struct manip {
                Ref ref;
                friend std::ostream& operator<<(std::ostream& os, manip const& m) {
                    pretty_io{os,""}(m.ref);
                    return os;
                }
            };
    
            std::ostream& _os;
            std::string _indent;
    
            void operator()(Value const& v) const {
                boost::apply_visitor(*this, v);
            }
            void operator()(Null) const {
                _os << "null";
            }
            void operator()(String const& s) const {
                _os << std::quoted(s);
            }
            void operator()(Property const& p) const {
                _os << '\n' << _indent; operator()(p.first);
                _os << ": ";            operator()(p.second);
            }
            void operator()(Object const& o) const {
                pretty_io nested{_os, _indent+"  "};
                _os << "{";
                bool first = true;
                for (auto& p : o) { first||_os << ","; nested(p); first = false; }
                _os << "\n" << _indent << "}";
            }
            void operator()(Array const& o) const {
                pretty_io nested{_os, _indent+"  "};
                _os << "[\n" << _indent << "  ";
                bool first = true;
                for (auto& p : o) { first||_os << ",\n" << _indent << "  "; nested(p); first = false; }
                _os << "\n" << _indent << "]";
            }
        };
    
        Value to_json(Value const& v) { return v; }
    
        template <typename T, typename V = decltype(to_json(std::declval<T const&>()))>
        pretty_io::manip<V> pretty(T const& v) { return {to_json(v)}; }
    }
    

    The to_json thing dubs as a handy ADL-enabled extension point, you can already us it now:

    std::cout << json::pretty("hello world"); // prints as a JSON String
    

    Connecting it up

    To make the following work:

    std::cout << json::pretty(plus1);
    

    All we need is the appropriate to_json overload. We could jot it all in there, but we might end up needing to "friend" a function named to_json, worse still, forward declare types from the json namespace (json::Value at the very least). That's too intrusive. So, let's add anothe tiny indirection:

    auto to_json(Expression const* expression) {
        return serialization::call(expression);
    }
    

    The trick is to hide the JSON stuff inside an opaque struct that we can then befriend: struct serialization. The rest is straightforward:

    struct serialization {
        static json::Value call(Expression const* e) {
            if (auto* f = dynamic_cast<Function const*>(e)) {
                json::Array args;
                for (auto& a : f->m_arguments)
                    args.push_back(call(a));
                return json::Object {
                    { "name", f->getName() },
                    { "type", "Function" },
                    { "arguments", args },
                };
            }
    
            if (auto* v = dynamic_cast<Value const*>(e)) {
                return json::Object {
                    { "name", v->getName() },
                    { "type", "Value" },
                    { "value", v->getValue() },
                };
            }
    
            return {}; // Null in case we didn't implement a node type
        }
    };
    

    Full Demo

    See it Live On Coliru

    #include <boost/lexical_cast.hpp>
    #include <iostream>
    #include <iomanip>
    #include <vector>
    
    struct Expression {
        virtual std::string getName() const = 0;
    };
    
    struct Value : Expression {
        virtual std::string getValue() const = 0;
    };
    
    struct IntegerValue : Value {
        IntegerValue(int value) : m_value(value) {}
        virtual std::string getName() const override { return "IntegerValue"; }
        virtual std::string getValue() const override { return boost::lexical_cast<std::string>(m_value); }
    
      private:
        int m_value;
    };
    
    struct Function : Expression {
        void addArgument(Expression *expression) { m_arguments.push_back(expression); }
        virtual std::string getName() const override { return m_name; }
    
      protected:
        std::vector<Expression *> m_arguments;
        std::string m_name;
    
        friend struct serialization;
    };
    
    struct Plus : Function {
        Plus() : Function() { m_name = "Plus"; }
    };
    
    ///////////////////////////////////////////////////////////////////////////////
    // A simple JSON facility
    #include <boost/variant.hpp>
    namespace json {
        // adhoc JSON rep
        struct Null {};
        using String = std::string;
    
        using Value = boost::make_recursive_variant<
            Null,
            String,
            std::vector<boost::recursive_variant_>,
            std::vector<std::pair<String, boost::recursive_variant_> >
        >::type;
    
        using Property = std::pair<String, Value>;
        using Object = std::vector<Property>;
        using Array = std::vector<Value>;
    }
    
    ///////////////////////////////////////////////////////////////////////////////
    // Pretty Print manipulator
    #include <iomanip>
    namespace json {
    
        // pretty print it
        struct pretty_io {
            using result_type = void;
    
            template <typename Ref>
            struct manip {
                Ref ref;
                friend std::ostream& operator<<(std::ostream& os, manip const& m) {
                    pretty_io{os,""}(m.ref);
                    return os;
                }
            };
    
            std::ostream& _os;
            std::string _indent;
    
            void operator()(Value const& v) const {
                boost::apply_visitor(*this, v);
            }
            void operator()(Null) const {
                _os << "null";
            }
            void operator()(String const& s) const {
                _os << std::quoted(s);
            }
            void operator()(Property const& p) const {
                _os << '\n' << _indent; operator()(p.first);
                _os << ": ";            operator()(p.second);
            }
            void operator()(Object const& o) const {
                pretty_io nested{_os, _indent+"  "};
                _os << "{";
                bool first = true;
                for (auto& p : o) { first||_os << ","; nested(p); first = false; }
                _os << "\n" << _indent << "}";
            }
            void operator()(Array const& o) const {
                pretty_io nested{_os, _indent+"  "};
                _os << "[\n" << _indent << "  ";
                bool first = true;
                for (auto& p : o) { first||_os << ",\n" << _indent << "  "; nested(p); first = false; }
                _os << "\n" << _indent << "]";
            }
        };
    
        Value to_json(Value const& v) { return v; }
    
        template <typename T, typename V = decltype(to_json(std::declval<T const&>()))>
        pretty_io::manip<V> pretty(T const& v) { return {to_json(v)}; }
    }
    
    ///////////////////////////////////////////////////////////////////////////////
    // Expression -> JSON
    struct serialization {
        static json::Value call(Expression const* e) {
            if (auto* f = dynamic_cast<Function const*>(e)) {
                json::Array args;
                for (auto& a : f->m_arguments)
                    args.push_back(call(a));
                return json::Object {
                    { "name", f->getName() },
                    { "type", "Function" },
                    { "arguments", args },
                };
            }
    
            if (auto* v = dynamic_cast<Value const*>(e)) {
                return json::Object {
                    { "name", v->getName() },
                    { "type", "Value" },
                    { "value", v->getValue() },
                };
            }
    
            return {};
        }
    };
    
    auto to_json(Expression const* expression) {
        return serialization::call(expression);
    }
    
    int main() {
        // Build expression 4 + 5 + 6 as 4 + (5 + 6)
        Function *plus1 = new Plus();
        Function *plus2 = new Plus();
        Value *iv4 = new IntegerValue(4);
        Value *iv5 = new IntegerValue(5);
        Value *iv6 = new IntegerValue(6);
        plus2->addArgument(iv5);
        plus2->addArgument(iv6);
        plus1->addArgument(iv4);
        plus1->addArgument(plus2);
    
        // Generate json string here, but how?
    
        std::cout << json::pretty(plus1);
    }
    

    Output is picture-perfect from your question:

    {
      "name": "Plus",
      "type": "Function",
      "arguments": [
        {
          "name": "IntegerValue",
          "type": "Value",
          "value": "4"
        },
        {
          "name": "Plus",
          "type": "Function",
          "arguments": [
            {
              "name": "IntegerValue",
              "type": "Value",
              "value": "5"
            },
            {
              "name": "IntegerValue",
              "type": "Value",
              "value": "6"
            }
          ]
        }
      ]
    }