sehe
sehe

Reputation: 393114

Populate nested structs with semantic actions

(question lifted from Spirit-general mailing list)

Hello,

I'm working in a parser with spirit qi. The grammar is working good, but I have some problems to populate my struct instance with Semantic Actions.

With the direct struct attributes, like "Request.id" and "Request.url", the code is working. But I don't know how to populate the attributes inside the nested struct "Info", neither how to push values in "Request.list".

Here is my code (The string to parse could have the values in any order):

#include <boost/spirit/include/qi.hpp>
#include <boost/fusion/include/adapt_struct.hpp>
#include <boost/spirit/include/phoenix.hpp>

#include <iostream>
#include <vector>
#include <string>

struct Request
{
    std::string id;
    std::string url;
    std::vector<std::string> list;

    struct Info
    {
        std::string id;
        std::string desc;
    };
    Info info;
};

BOOST_FUSION_ADAPT_STRUCT(
    Request,
    (std::string, id)
)

template <typename Iterator>
struct request_parser : boost::spirit::qi::grammar<Iterator, Request(), boost::spirit::ascii::space_type>
{      
        request_parser() : request_parser::base_type(start)
        {
            using namespace boost::spirit::qi;
            namespace ascii = boost::spirit::ascii;
            namespace phx = boost::phoenix;

            quoted_string %= lexeme['"' >> +(char_ - '"') >> '"'];

            start %=
                BR_OP
                >>((DQUOTE >> lit(INFO) >> DQUOTE >> COLON
                    >> BR_OP
                    >> ((DQUOTE >> lit(DESC) >> DQUOTE >> COLON >> quoted_string)
                    ^ (DQUOTE >> lit(ID) >> DQUOTE >> COLON >> quoted_string)) % COMMA
                    >> BR_CL)
                ^(DQUOTE >> lit(ID) >> DQUOTE >> COLON >> quoted_string[phx::bind(&Request::id, _val) = _1])
                ^(DQUOTE >> lit(URL) >> DQUOTE >> COLON >> quoted_string[phx::bind(&Request::url, _val) = _1])
                ^(DQUOTE >> lit(LIST) >> DQUOTE >> COLON
                    >> SQ_OP
                    >> quoted_string % COMMA
                    >> SQ_CL)) % COMMA
                >>
                BR_CL
                ;
        }

        boost::spirit::qi::rule<Iterator, std::string(), boost::spirit::ascii::space_type> quoted_string;
        boost::spirit::qi::rule<Iterator, Request(), boost::spirit::ascii::space_type> start;

        char BR_OP = '{';
        char BR_CL = '}';
        char DQUOTE = '"';
        char COLON = ':';
        char SQ_OP = '[';
        char SQ_CL = ']';
        char COMMA = ',';

        const char* LIST = "list";
        const char* ID = "id";
        const char* URL = "url";
        const char* INFO = "info";
        const char* DESC = "desc";
};

int main()
{
    typedef std::string::iterator iterator_type;
    typedef request_parser<iterator_type> requester_parser;

    std::string str = "{\"list\":[\"data1\",\"data2\"],\"info\":{\"desc\":\"description\",\"id\":\"23\"},\"id\":\"1234\",\"url\":\"ok.com\"}";

    Request rqst;
    requester_parser parser;

    using boost::spirit::ascii::space;
    boost::spirit::qi::phrase_parse(str.begin(), str.end(), parser, space, rqst);

    using std::cout;
    using std::endl;
    cout << rqst.id << endl;
    cout << rqst.url << endl;
    cout << rqst.list.size() << endl;
    cout << rqst.info.id << endl;
    cout << rqst.info.desc << endl;
}

Thanks! Emiliano

Upvotes: 2

Views: 187

Answers (1)

sehe
sehe

Reputation: 393114

As always, I'm going to suggest the solution not using semantic actions (Boost Spirit: "Semantic actions are evil"?). I truly believe semantic-actions should be used sparingly as they mostly have potential to complicate things. (Like new and delete, there are very limited occasions where you should be using them).

In this case, you're nearly there: automatic attribute propagation synthesizes a tuple of permutation members in sequence.

So, if you just adapt your struct in the order of your grammar, you'll be fine:

BOOST_FUSION_ADAPT_STRUCT(Request::Info, id, desc)
BOOST_FUSION_ADAPT_STRUCT(Request, info, id, url, list)

The grammar itself had some issues ((a ^ b) % ',' does not do something similar to (a | b) % ',': it would parse a, b, ab, ba, a,ba,b etc.).

I split the rules up a bit to remove redundancy and used a "smart" delimiter to expect commas in the right places:

delim_  = &lit('}') | ','; // unless at end of block, expect a comma

The full grammar became:

string_ = '"' >> *~char_('"') >> '"';
key_    = '"' >> string(_r1) >> '"';
prop_   = key_(_r1) >> ':' >> string_ >> delim_;

info_ = key_("info"s) >> ':'
    >> '{'
    >> (prop_("desc"s) ^ prop_("id"s))
    >> '}'
    >> delim_
    ;

list_ = key_("list"s) >> ':'
    >> '[' >> string_ % ',' >> ']'
    >> delim_
    ;

request_ = '{' >> (info_ ^ prop_("id"s) ^ prop_("url"s) ^ list_) >> '}';

Note

  • how it doesn't use any semantic actions.
  • I used inherited attributes to reuse key_ and prop_. You could use the Nabialek trick to further generalize for non-string properties
    start    = skip(space) [ request_ ];
  • I opted to hide the skipper from the external interface (the skipper is part of the grammar specification, so it should probably not be specified by the caller).

Demo

Live On Coliru

#include <boost/spirit/include/qi.hpp>
#include <boost/fusion/include/adapt_struct.hpp>

#include <iostream>
#include <vector>
#include <string>
using namespace std::literals::string_literals;

struct Request
{
    std::string id;
    std::string url;
    std::vector<std::string> list;

    struct Info {
        std::string id;
        std::string desc;
    };
    Info info;
};

BOOST_FUSION_ADAPT_STRUCT(Request::Info, id, desc)
BOOST_FUSION_ADAPT_STRUCT(Request, info, id, url, list)

template <typename Iterator>
struct request_parser : boost::spirit::qi::grammar<Iterator, Request()>
{      
    request_parser() : request_parser::base_type(start)
    {
        using namespace boost::spirit::qi;

        delim_  = &lit('}') | ',';
        string_ = '"' >> *~char_('"') >> '"';
        key_    = '"' >> string(_r1) >> '"';
        prop_   = key_(_r1) >> ':' >> string_ >> delim_;

        info_ = key_("info"s) >> ':'
            >> '{'
            >> (prop_("desc"s) ^ prop_("id"s))
            >> '}'
            >> delim_
            ;

        list_ = key_("list"s) >> ':'
            >> '[' >> string_ % ',' >> ']'
            >> delim_
            ;

        request_ = '{' >> (info_ ^ prop_("id"s) ^ prop_("url"s) ^ list_) >> '}';
        start    = skip(space) [ request_ ];
    }

  private:
    using Skipper = boost::spirit::qi::space_type;
    boost::spirit::qi::rule<Iterator, Request()> start;
    boost::spirit::qi::rule<Iterator, Request(), Skipper> request_;
    boost::spirit::qi::rule<Iterator, Request::Info(), Skipper> info_;
    boost::spirit::qi::rule<Iterator, std::vector<std::string>(), Skipper> list_;
    boost::spirit::qi::rule<Iterator, std::string(std::string), Skipper> prop_;

    // lexemes
    boost::spirit::qi::rule<Iterator, std::string()> string_;
    // literals
    boost::spirit::qi::rule<Iterator, void(std::string)> key_;
    boost::spirit::qi::rule<Iterator> delim_;
};

int main()
{
    typedef std::string::iterator iterator_type;
    typedef request_parser<iterator_type> requester_parser;

    std::string str = R"({
    "list": ["data1", "data2"],
    "info": {
        "desc": "description",
        "id": "23"
    },
    "id": "1234",
    "url": "ok.com"
})";

    Request parsed;
    requester_parser parser;
    parse(str.begin(), str.end(), parser, parsed);

    std::cout << "parsed.id:          " << parsed.id          << "\n";
    std::cout << "parsed.url:         " << parsed.url         << "\n";
    std::cout << "parsed.list.size(): " << parsed.list.size() << "\n";
    std::cout << "parsed.info.id:     " << parsed.info.id     << "\n";
    std::cout << "parsed.info.desc:   " << parsed.info.desc   << "\n";
}

Prints:

parsed.id:          1234
parsed.url:         ok.com
parsed.list.size(): 2
parsed.info.id:     description
parsed.info.desc:   23

Upvotes: 6

Related Questions