Search code examples
c++boostboost-spiritboost-fusionboost-spirit-karma

boost::spirit::karma grammar: Comma delimited output from struct with optionals attributes


I need a comma delimited output from a struct with optionals. For example, if I have this struct:

MyStruct 
{ 
    boost::optional<std::string> one;
    boost::optional<int> two;
    boost::optional<float> three;
};

An output like: { "string", 1, 3.0 } or { "string" } or { 1, 3.0 } and so on.

Now, I have code like this:

struct MyStruct
{
    boost::optional<std::string> one;
    boost::optional<int> two;
    boost::optional<float> three;
};

BOOST_FUSION_ADAPT_STRUCT
(MyStruct,
one,
two,
three)

template<typename Iterator>
struct MyKarmaGrammar : boost::spirit::karma::grammar<Iterator, MyStruct()>
{
    MyKarmaGrammar() : MyKarmaGrammar::base_type(request_)
    {
        using namespace std::literals::string_literals;
        namespace karma = boost::spirit::karma;

        using karma::int_;
        using karma::double_;
        using karma::string;
        using karma::lit;
        using karma::_r1;

        key_ = '"' << string(_r1) << '"';

        str_prop_ = key_(_r1) << ':' 
            << string
            ;

        int_prop_ = key_(_r1) << ':' 
            << int_
            ;

        dbl_prop_ = key_(_r1) << ':' 
            << double_
            ;

        //REQUEST
        request_ = '{'
            <<  -str_prop_("one"s) <<
                -int_prop_("two"s) <<
                -dbl_prop_("three"s)
            << '}'
            ;
    }

private:
    //GENERAL RULES
    boost::spirit::karma::rule<Iterator, void(std::string)> key_;
    boost::spirit::karma::rule<Iterator, double(std::string)> dbl_prop_;
    boost::spirit::karma::rule<Iterator, int(std::string)> int_prop_;
    boost::spirit::karma::rule<Iterator, std::string(std::string)> str_prop_;

    //REQUEST
    boost::spirit::karma::rule<Iterator, MyStruct()> request_;
};

int main()
{
    using namespace std::literals::string_literals;

    MyStruct request = {std::string("one"), 2, 3.1};

    std::string generated;
    std::back_insert_iterator<std::string> sink(generated);

    MyKarmaGrammar<std::back_insert_iterator<std::string>> serializer;

    boost::spirit::karma::generate(sink, serializer, request);

    std::cout << generated << std::endl;
}

This works but I need a comma delimited output. I tried with a grammar like:

request_ = '{'
    <<  (str_prop_("one"s) |
        int_prop_("two"s) |
        dbl_prop_("three"s)) % ','
    << '}'
    ;

But I receive this compile error:

/usr/include/boost/spirit/home/support/container.hpp:194:52: error: no type named ‘const_iterator’ in ‘struct MyStruct’
         typedef typename Container::const_iterator type;

thanks!


Solution

  • Your struct is not a container, therefore list-operator% will not work. The documentation states it expects the attribute to be a container type.

    So, just like in the Qi counterpart I showed you to create a conditional delim production:

     delim         = (&qi::lit('}')) | ',';
    

    You'd need something similar here. However, everything about it is reversed. Instead of "detecting" the end of the input sequence from the presence of a {, we need to track the absense of preceding field from "not having output a field since opening brace yet".

    That's a bit trickier since the required state cannot come from the same source as the input. We'll use a parser-member for simplicity here¹:

    private:
      bool _is_first_field;
    

    Now, when we generate the opening brace, we want to initialize that to true:

        auto _f = px::ref(_is_first_field); // short-hand
        request_ %= lit('{') [ _f = true ]
    

    Note: Use of %= instead of = tells Spirit that we want automatic attribute propagation to happen, in spite of the presence of a Semantic Action ([ _f = true ]).

    Now, we need to generate the delimiter:

        delim = eps(_f) | ", ";
    

    Simple. Usage is also simple, except we'll want to conditionally reset the _f:

        auto reset = boost::proto::deep_copy(eps [ _f = false ]);
    
        str_prop_ %= (delim << key_(_r1) << string  << reset) | ""; 
        int_prop_ %= (delim << key_(_r1) << int_    << reset) | ""; 
        dbl_prop_ %= (delim << key_(_r1) << double_ << reset) | ""; 
    

    A very subtle point here is that I changed to the declared rule attribute types from T to optional<T>. This allows Karma to do the magic to fail the value generator if it's empty (boost::none), and skipping the reset!

    ka::rule<Iterator, boost::optional<double>(std::string)> dbl_prop_;
    ka::rule<Iterator, boost::optional<int>(std::string)> int_prop_;
    ka::rule<Iterator, boost::optional<std::string>(std::string)> str_prop_;
    

    Now, let's put together some testcases:

    Test Cases

    Live On Coliru

    #include "iostream"
    #include <boost/optional/optional_io.hpp>
    #include <boost/fusion/include/io.hpp>
    #include <boost/spirit/include/karma.hpp>
    #include <boost/spirit/include/phoenix.hpp>
    #include <string>
    
    struct MyStruct {
        boost::optional<std::string> one;
        boost::optional<int> two;
        boost::optional<double> three;
    };
    
    BOOST_FUSION_ADAPT_STRUCT(MyStruct, one, two, three)
    
    namespace ka = boost::spirit::karma;
    namespace px = boost::phoenix;
    
    template<typename Iterator>
    struct MyKarmaGrammar : ka::grammar<Iterator, MyStruct()> {
        MyKarmaGrammar() : MyKarmaGrammar::base_type(request_) {
            using namespace std::literals::string_literals;
    
            using ka::int_;
            using ka::double_;
            using ka::string;
            using ka::lit;
            using ka::eps;
            using ka::_r1;
    
            auto _f    = px::ref(_is_first_field);
            auto reset = boost::proto::deep_copy(eps [ _f = false ]);
    
            key_ = '"' << string(_r1) << "\":";
    
            delim = eps(_f) | ", ";
    
            str_prop_ %= (delim << key_(_r1) << string  << reset) | ""; 
            int_prop_ %= (delim << key_(_r1) << int_    << reset) | ""; 
            dbl_prop_ %= (delim << key_(_r1) << double_ << reset) | ""; 
    
            //REQUEST
            request_ %= lit('{') [ _f = true ]
                <<  str_prop_("one"s) <<
                    int_prop_("two"s) <<
                    dbl_prop_("three"s)
                << '}';
        }
    
      private:
        bool _is_first_field = true;
        //GENERAL RULES
        ka::rule<Iterator, void(std::string)> key_;
        ka::rule<Iterator, boost::optional<double>(std::string)> dbl_prop_;
        ka::rule<Iterator, boost::optional<int>(std::string)> int_prop_;
        ka::rule<Iterator, boost::optional<std::string>(std::string)> str_prop_;
        ka::rule<Iterator> delim;
    
        //REQUEST
        ka::rule<Iterator, MyStruct()> request_;
    };
    
    template <typename T> std::array<boost::optional<T>, 2> option(T const& v) {
        return { { v, boost::none } };
    }
    
    int main() {
        using namespace std::literals::string_literals;
    
        for (auto a : option("one"s))
        for (auto b : option(2))
        for (auto c : option(3.1))
        for (auto request : { MyStruct { a, b, c } }) {
            std::string generated;
            std::back_insert_iterator<std::string> sink(generated);
    
            MyKarmaGrammar<std::back_insert_iterator<std::string>> serializer;
    
            ka::generate(sink, serializer, request);
    
            std::cout << boost::fusion::as_vector(request) << ":\t" << generated << "\n";
        }
    }
    

    Printing:

    ( one  2  3.1): {"one":one, "two":2, "three":3.1}
    ( one  2 --):   {"one":one, "two":2}
    ( one --  3.1): {"one":one, "three":3.1}
    ( one -- --):   {"one":one}
    (--  2  3.1):   {"two":2, "three":3.1}
    (--  2 --):     {"two":2}
    (-- --  3.1):   {"three":3.1}
    (-- -- --):     {}
    

    ¹ Note this limits re-entrant use of the parser, as well as making it non-const etc. karma::locals are the true answer to that, adding a little more complexity