Search code examples
c++boost-spiritabnf

Parse into a complex struct with boost::spirit


I have an input encoded with ABNF grammar rules (it is MEGACO protocol):

!/3 [15.232.33.21]:2134
T=173619123
{
    C=230234621
    {
        PR=9,
        MF=ip/187/6/23045241
        {
           ...
        },
        MF=ip/187/6/23045242
        {

I want to parse it into a complex struct with boost::spirit, pseudo-code:

megaco
{
    param1
    param2
    {
        param1
        param2
        list of param3
            param3-1
            {
                param4
                ...
            }
            param3-2
            {
                ...
            }
    }
}

It should be noted that the grammar is complex and contains a lot of alternatives, levels and sequences. I am not sure how to create a boost::spirit parser to decode such a message level by level saving necessary values in the process.

Also I am not sure that boost::spirit is the right tool for it.

So how should I do it?

UPDATE: I want to thanks sehe for the great example, still the main task is to populate some structure with the values from the message. The only possible way I see now is to use BOOST_FUSION_ADAPT_STRUCT with a lot of structs using boost::variant for alternative and std::vector for lists of items. Am I right?


Solution

  • Ironically, heading over to Appendix B.2 of RFC 3525, and implementing the bulk of it superficially¹ it turns out your sample snippet is invalid.

    It was missing ammParameter, that's not optional as soon as you have the opening {:

    enter image description here

    So, fixing the snippet, here's something to get you started. It's only 850 lines of code because the spec is lengthy:

    Live On Coliru

    //#define BOOST_SPIRIT_DEBUG
    #include <boost/spirit/include/qi.hpp>
    namespace qi = boost::spirit::qi;
    
    template <typename It> struct Megaco {
    
    struct Tokens {
        Tokens() {
            using namespace qi;
            auto SafeChar = copy(alnum | char_("-+&!_/'?@^`~*$\\()%|."));
            auto RestChar = copy(char_(";[]{}:,#<>="));
            auto WSP         = copy(char_(" \t"));
            COMMENT      = ';' >> *(SafeChar | RestChar | WSP | '"') >> (eol|eoi);
    
            SEP = +(WSP | COMMENT | eol);
            LWSP = *(WSP | COMMENT | eol);
            NAME = raw [ alpha >> repeat(0,63) [alnum|'_'] ];
    
            // Note - The double-quote character is not allowed in quotedString.
            quotedString = '"' >> *(SafeChar | RestChar | WSP) >> '"';
            VALUE       = quotedString | lexeme[+SafeChar];
            octetString = *("\\}" | char_("\x01-\x7C\x7E-\xFF"));
        
            MTPToken=no_case["MTP"];
            H221Token=no_case["H221"];
            H223Token=no_case["H223"];
            H226Token=no_case["H226"];
            V18Token=no_case["V18"];
            V22Token=no_case["V22"];
            V22bisToken=no_case["V22b"];
            V32Token=no_case["V32"];
            V32bisToken=no_case["V32b"];
            V34Token=no_case["V34"];
            V76Token=no_case["V76"];
            V90Token=no_case["V90"];
            V91Token=no_case["V91"];
    
            BOOST_SPIRIT_DEBUG_NODES(
                    // snipped for Stack Overflow, see Coliru
               )
        }
      protected:
        using Token = qi::rule<It>;
        Token COMMENT, SEP, LWSP;
        qi::rule<It, std::string()> NAME, quotedString, VALUE, octetString;
        #define TOK(name,long,short) name##Token{qi::no_case[qi::lit(#long)|#short]},
        Token
            TOK(Add,Add,A) TOK(Audit,Audit,AT) TOK(AuditCap,AuditCapability,AC) TOK(AuditValue,AuditValue,AV) TOK(Auth,Authentication,AU)
            TOK(Bothway,Bothway,BW) TOK(Brief,Brief,BR) TOK(Buffer,Buffer,BF) TOK(Ctx,Context,C) TOK(ContextAudit,ContextAudit,CA)
            TOK(DigitMap,DigitMap,DM) TOK(Disconnected,Disconnected,DC) TOK(Delay,Delay,DL) TOK(Duration,Duration,DR) TOK(Embed,Embed,EM)
            TOK(Emergency,Emergency,EG) TOK(Error,Error,ER) TOK(EventBuffer,EventBuffer,EB) TOK(Events,Events,E) TOK(Failover,Failover,FL)
            TOK(Forced,Forced,FO) TOK(Graceful,Graceful,GR) TOK(HandOff,HandOff,HO) TOK(ImmAckRequired,ImmAckRequired,IA)
            TOK(Inactive,Inactive,IN) TOK(Isolate,Isolate,IS) TOK(InSvc,InService,IV) TOK(InterruptByEvent,IntByEvent,IBE)
            TOK(InterruptByNewSignalsDescr,IntBySigDescr,IBS) TOK(KeepActive,KeepActive,KA) TOK(Local,Local,L) TOK(LocalControl,LocalControl,O)
            TOK(LockStep,LockStep,SP) TOK(Loopback,Loopback,LB) TOK(Media,Media,M) TOK(Megacop,MEGACO,!) TOK(Method,Method,MT)
            TOK(MgcId,MgcIdToTry,MG) TOK(Mode,Mode,MO) TOK(Modify,Modify,MF) TOK(Modem,Modem,MD) TOK(Move,Move,MV)
            TOK(Mux,Mux,MX) TOK(Notify,Notify,N) TOK(NotifyCompletion,NotifyCompletion,NC) TOK(ObservedEvents,ObservedEvents,OE) TOK(Oneway,Oneway,OW)
            TOK(OnOff,OnOff,OO) TOK(OtherReason,OtherReason,OR) TOK(OutOfSvc,OutOfService,OS) TOK(Packages,Packages,PG) TOK(Pending,Pending,PN)
            TOK(Priority,Priority,PR) TOK(Profile,Profile,PF) TOK(Reason,Reason,RE) TOK(Recvonly,ReceiveOnly,RC) TOK(Reply,Reply,P)
            TOK(Restart,Restart,RS) TOK(Remote,Remote,R) TOK(ReservedGroup,ReservedGroup,RG) TOK(ReservedValue,ReservedValue,RV) TOK(Sendonly,SendOnly,SO)
            TOK(Sendrecv,SendReceive,SR) TOK(Services,Services,SV) TOK(ServiceStates,ServiceStates,SI) TOK(ServiceChange,ServiceChange,SC)
            TOK(ServiceChangeAddress,ServiceChangeAddress,AD) TOK(SignalList,SignalList,SL) TOK(Signals,Signals,SG) TOK(SignalType,SignalType,SY)
            TOK(Stats,Statistics,SA) TOK(Stream,Stream,ST) TOK(Subtract,Subtract,S) TOK(SynchISDN,SynchISDN,SN) TOK(TerminationState,TerminationState,TS)
            TOK(Test,Test,TE) TOK(TimeOut,TimeOut,TO) TOK(Topology,Topology,TP) TOK(Trans,Transaction,T) TOK(ResponseAck,TransactionResponseAck,K)
            TOK(Version,Version,V) MTPToken, H221Token, H223Token, H226Token, V18Token, V22Token, V22bisToken,
            V32Token, V32bisToken, V34Token, V76Token, V90Token, V91Token;
    
        // The values 0x0, 0xFFFFFFFE and 0xFFFFFFFF are reserved.
        struct SpecialContexts : qi::symbols<char, uint32_t> {
            enum { NULL_, CHOOSE = 0xFFFFFFFEl, ALL = 0xFFFFFFFFl };
            SpecialContexts() { this->add
                ("$", CHOOSE)
                ("*", ALL)
                ("-", NULL_);
            }
        } SpecialContext;
    };
    
    struct Parser : Tokens, qi::grammar<It> {
        using Tokens::LWSP;
        using Tokens::SEP;
        using Tokens::SpecialContext;
        using Tokens::NAME;
        using Tokens::VALUE;
        using Tokens::quotedString;
        using Tokens::octetString;
    
        Parser() : Parser::base_type(megacoMessage) {
            using namespace qi;
    
            auto LBRKT   = copy(LWSP >> '{' >> LWSP);
            auto RBRKT   = copy(LWSP >> '}' >> LWSP);
            auto EQUAL   = copy(LWSP >> '=' >> LWSP);
            auto COMMA   = copy(LWSP >> ',' >> LWSP);
            auto INEQUAL = copy(LWSP >> char_("><#") >> LWSP);
            auto LSBRKT  = copy(LWSP >> '[' >> LWSP);
            auto RSBRKT  = copy(LWSP >> ']' >> LWSP);
            auto LPAREN  = copy(LWSP >> '(' >> LWSP);
            auto RPAREN  = copy(LWSP >> ')' >> LWSP);
            auto PIPE    = copy(LWSP >> '|' >> LWSP);
    
            megacoMessage = LWSP >> -(authenticationHeader >> SEP) >> message;
    
            authenticationHeader 
                = Tokens::AuthToken
                >> EQUAL
                >> ("0x" >> repeat(8)[xdigit]) 
                >> ':' 
                >> ("0x" >> repeat(8)[xdigit])
                >> ':' 
                >> repeat(24,64)[xdigit]
                ;
    
            message 
                = Tokens::MegacopToken >> '/' >> Version >> SEP >> mId >> SEP
                >> messageBody;
    
            mId
                = ((domainAddress | domainName) >> -(':' >> portNumber)) 
                | mtpAddress 
                | deviceName
                ;
    
            domainName           
                = '<' >> alnum >> repeat(0,63)[char_("a-zA-Z0-9.-")] >> '>';
            deviceName
                = pathNAME.alias();
            pathNAME // TODO total lenght limit according to RFC comment
                = -lit('*') >> NAME >> *char_("/*a-zA-Z0-9_$") >> -('@' >> pathDomainName);
    
            pathDomainName = raw [(alnum|'*') >> repeat(0,63)[alnum|'-'|'*'|'.'] ];
    
            ContextID = SpecialContext | UINT32;
    
            domainAddress = '[' >> (IPv4address | IPv6address) >> ']';
    
            // RFC2373 contains the definition of IP6Addresses.
            IPv6address   = hexpart >> - (':' >> IPv4address);
            IPv4address   = V4hex >> '.' >> V4hex >> '.' >> V4hex >> '.' >> V4hex;
            V4hex         = qi::uint_parser<uint8_t, 10, 1, 3>{}; // "0".."255"
    
            hexpart = raw [
                      "::" >> -hexseq
                    | hexseq >> "::" >> -hexseq
                    | hexseq
                ];
    
            hexseq = raw[ uint_parser<uint16_t, 16, 1, 4>{} % ':' ];
    
            portNumber = UINT16;
    
            // TODO constraint checking?
            // To octet align the mtpAddress the MSBs shall be encoded as 0s.
            // An octet shall be represented by 2 hex digits.
            mtpAddress 
                = Tokens::MTPToken 
                >> LBRKT
                >> uint_parser<uint32_t, 16, 4, 8>{}
                >> RBRKT
                ;
    
            messageBody = errorDescriptor | transactionList;
    
            transactionList      = +( transactionRequest | transactionReply |
                                   transactionPending | transactionResponseAck );
            //Use of response acks is dependent on underlying transport
    
            transactionPending   = Tokens::PendingToken >> EQUAL >> transactionID >> LBRKT >> RBRKT;
    
            transactionResponseAck = Tokens::ResponseAckToken 
                >> LBRKT >> (transactionAck % COMMA) >> RBRKT;
            transactionAck = transactionID | (transactionID >> "-" >> transactionID);
    
            transactionRequest   = Tokens::TransToken >> EQUAL >> transactionID >> LBRKT
                                   >> (actionRequest % COMMA) >> RBRKT;
    
            actionRequest        = Tokens::CtxToken >> EQUAL >> ContextID >> LBRKT >> ((
                                   contextRequest >> -(COMMA  >> commandRequestList))
                                   | commandRequestList) >> RBRKT;
    
            contextRequest    = ((contextProperties >> -(COMMA >> contextAudit))
                        | contextAudit);
    
            contextProperties    = contextProperty % COMMA;
    
            // at-most-once
            contextProperty    = (topologyDescriptor | priority | Tokens::EmergencyToken);
    
            contextAudit   = Tokens::ContextAuditToken 
                >> LBRKT >> (contextAuditProperties % COMMA) >> RBRKT;
    
            // at-most-once
            contextAuditProperties = ( Tokens::TopologyToken | Tokens::EmergencyToken | Tokens::PriorityToken );
    
            // "O-" indicates an optional command
            // "W-" indicates a wildcarded response to a command
            commandRequestList = -lit("O-") >> -lit("W-") >> commandRequest
                                 >> *(COMMA >> -lit("O-") >> -lit("W-") >> commandRequest);
    
            commandRequest      = ( ammRequest | subtractRequest | auditRequest |
                                    notifyRequest | serviceChangeRequest);
    
            transactionReply     = Tokens::ReplyToken >> EQUAL >> transactionID >> LBRKT
                              >> -( Tokens::ImmAckRequiredToken >> COMMA)
                            >> ( errorDescriptor | actionReplyList ) >> RBRKT;
    
            actionReplyList      = actionReply % COMMA ;
    
    
            actionReply          = Tokens::CtxToken >> EQUAL >> ContextID >> LBRKT
                              >> ( ( errorDescriptor | commandReply ) |
                     (commandReply >> COMMA >> errorDescriptor) ) >> RBRKT;
    
            commandReply      = (( contextProperties >> -(COMMA >> commandReplyList) ) |
                                    commandReplyList );
    
    
            commandReplyList     = commandReplys % COMMA ;
    
            commandReplys        = (serviceChangeReply | auditReply | ammsReply |
                                    notifyReply );
    
            //Add Move and Modify have the same request parameters
            ammRequest           = (Tokens::AddToken | Tokens::MoveToken | Tokens::ModifyToken ) >> EQUAL
                                    >> TerminationID >> 
                                    -(LBRKT >> (ammParameter % COMMA) >> RBRKT);
    
            //at-most-once
            ammParameter         = (mediaDescriptor | modemDescriptor |
                                    muxDescriptor | eventsDescriptor |
                                    signalsDescriptor | digitMapDescriptor |
                                    eventBufferDescriptor | auditDescriptor);
    
            ammsReply            = (Tokens::AddToken | Tokens::MoveToken | Tokens::ModifyToken |
                                    Tokens::SubtractToken ) >> EQUAL >> TerminationID >> -( LBRKT
                                    >> terminationAudit >> RBRKT );
    
            subtractRequest      =  Tokens::SubtractToken >> EQUAL >> TerminationID
                                    >> -( LBRKT >> auditDescriptor >> RBRKT);
    
            auditRequest         =  (Tokens::AuditValueToken | Tokens::AuditCapToken ) >> EQUAL
                                    >> TerminationID >> LBRKT >> auditDescriptor >> RBRKT;
    
            auditReply           = (Tokens::AuditValueToken | Tokens::AuditCapToken )
                                   >> ( contextTerminationAudit  | auditOther);
    
            auditOther           = EQUAL >> TerminationID >> -(LBRKT >> terminationAudit >> RBRKT);
    
            terminationAudit = auditReturnParameter % COMMA;
    
            contextTerminationAudit = EQUAL >> Tokens::CtxToken >> ( terminationIDList |
                                   LBRKT >> errorDescriptor >> RBRKT );
    
            auditReturnParameter = (mediaDescriptor | modemDescriptor |
                                    muxDescriptor | eventsDescriptor |
                                    signalsDescriptor | digitMapDescriptor |
    
                               observedEventsDescriptor | eventBufferDescriptor |
                                    statisticsDescriptor | packagesDescriptor |
                                     errorDescriptor | auditItem);
    
            auditDescriptor      = Tokens::AuditToken >> LBRKT >> -( auditItem % COMMA ) >> RBRKT;
    
            notifyRequest        = Tokens::NotifyToken >> EQUAL >> TerminationID
                                   >> LBRKT >> ( observedEventsDescriptor
                                         >> -( COMMA >> errorDescriptor ) ) >> RBRKT;
    
            notifyReply          = Tokens::NotifyToken >> EQUAL >> TerminationID
                                   >> -( LBRKT >> errorDescriptor >> RBRKT );
    
            serviceChangeRequest = Tokens::ServiceChangeToken >> EQUAL >> TerminationID
                                   >> LBRKT >> serviceChangeDescriptor >> RBRKT;
    
            serviceChangeReply   = Tokens::ServiceChangeToken >> EQUAL >> TerminationID
                                   >> -(LBRKT >> (errorDescriptor | serviceChangeReplyDescriptor) >> RBRKT);
    
            errorDescriptor   = Tokens::ErrorToken >> EQUAL >> ErrorCode
                                >> LBRKT >> -quotedString >> RBRKT;
    
            ErrorCode            = repeat(1,4)[digit]; // could be extended
    
            transactionID        = UINT32;
    
            // OTHER STUFF, DESCRIPTORS
            terminationIDList  = LBRKT >> (TerminationID % COMMA) >> RBRKT;
    
            TerminationID        = "ROOT" | pathNAME | "$" | "*";
    
            mediaDescriptor = Tokens::MediaToken >> LBRKT >> (mediaParm % COMMA) >> RBRKT;
    
            // at-most one terminationStateDescriptor
            // and either streamParm(s) or streamDescriptor(s) but not both
            mediaParm            = (streamParm | streamDescriptor | terminationStateDescriptor);
    
            // at-most-once per item
            streamParm           = (localDescriptor | remoteDescriptor | localControlDescriptor);
    
            streamDescriptor     = Tokens::StreamToken >> EQUAL >> StreamID 
                >> LBRKT >> streamParm >> *(COMMA >> streamParm) >> RBRKT;
    
            localControlDescriptor = Tokens::LocalControlToken 
                >> LBRKT >> localParm >> *(COMMA >> localParm) >> RBRKT;
    
            // at-most-once per item except for propertyParm
            localParm = (streamMode | propertyParm | reservedValueMode | reservedGroupMode);
    
    
            reservedValueMode    = Tokens::ReservedValueToken >> EQUAL >> ( lit("ON") | "OFF" );
            reservedGroupMode    = Tokens::ReservedGroupToken >> EQUAL >> ( lit("ON") | "OFF" );
    
            streamMode           = Tokens::ModeToken >> EQUAL >> streamModes;
    
            streamModes     = (Tokens::SendonlyToken | Tokens::RecvonlyToken | Tokens::SendrecvToken |
                                   Tokens::InactiveToken | Tokens::LoopbackToken );
    
            propertyParm         = pkgdName >> parmValue;
            parmValue            = (EQUAL >> alternativeValue | INEQUAL >> VALUE);
            alternativeValue     = ( VALUE
                           | LSBRKT >> (VALUE % COMMA) >> RSBRKT // sublist (i.e., A AND B AND ...)
                           | LBRKT >> (VALUE % COMMA) >> RBRKT // alternatives (i.e., A OR B OR ...)
                           | LSBRKT >> VALUE >> ':' >> VALUE >> RSBRKT ) // range
                           ;
            // Note - The octet zero is not among the permitted characters in
            // octet string.  As the current definition is limited to SDP, and a
            // zero octet would not be a legal character in SDP, this is not a
            // concern.
    
            localDescriptor      = Tokens::LocalToken >> LBRKT >> octetString >> RBRKT;
    
            remoteDescriptor     = Tokens::RemoteToken >> LBRKT >> octetString >> RBRKT;
    
            eventBufferDescriptor= Tokens::EventBufferToken >> -( LBRKT >> eventSpec >> *( COMMA >> eventSpec) >> RBRKT );
    
            eventSpec      = pkgdName >> -( LBRKT >> (eventSpecParameter % COMMA) >> RBRKT );
            eventSpecParameter   = (eventStream | eventOther);
    
            eventBufferControl     = Tokens::BufferToken >> EQUAL >> ( "OFF" | Tokens::LockStepToken );
    
            terminationStateDescriptor = Tokens::TerminationStateToken >> LBRKT
                       >> terminationStateParm >> *( COMMA >> terminationStateParm ) >> RBRKT;
    
            // at-most-once per item except for propertyParm
            terminationStateParm = (propertyParm | serviceStates | eventBufferControl);
    
    
            serviceStates        = Tokens::ServiceStatesToken >> EQUAL >> ( Tokens::TestToken | Tokens::OutOfSvcToken | Tokens::InSvcToken );
    
            muxDescriptor        = Tokens::MuxToken >> EQUAL >> MuxType  >> terminationIDList;
    
            MuxType              = ( Tokens::H221Token | Tokens::H223Token | Tokens::H226Token | Tokens::V76Token
                                    | extensionParameter );
    
            StreamID             = UINT16;
            pkgdName     = (PackageName >> '/' >> ItemID) //specific item
                         | (PackageName >> "/*") //all items in package
                         | "*/*" // all items supported by the MG
                         ;
            PackageName          = NAME.alias();
            ItemID               = NAME.alias();
    
            eventsDescriptor     = Tokens::EventsToken >> -( EQUAL >> RequestID >> LBRKT >> requestedEvent >> *( COMMA >> requestedEvent ) >> RBRKT );
    
            requestedEvent       = pkgdName >> -( LBRKT >> eventParameter >> *( COMMA >> eventParameter ) >> RBRKT );
    
            // at-most-once each of KeepActiveToken , eventDM and eventStream
            //at most one of either embedWithSig or embedNoSig but not both
            //KeepActiveToken and embedWithSig must not both be present
            eventParameter       = ( embedWithSig | embedNoSig | Tokens::KeepActiveToken | eventDM | eventStream | eventOther );
    
            embedWithSig         = Tokens::EmbedToken >> LBRKT >> signalsDescriptor
                                     >> -(COMMA >> embedFirst ) >> RBRKT;
            embedNoSig        = Tokens::EmbedToken >> LBRKT >> embedFirst >> RBRKT;
    
            // at-most-once of each
            embedFirst      = Tokens::EventsToken >> -( EQUAL >> RequestID >> LBRKT >> (secondRequestedEvent % COMMA) >> RBRKT );
    
            secondRequestedEvent = pkgdName >> -( LBRKT >> secondEventParameter >> *( COMMA >> secondEventParameter ) >> RBRKT );
    
            // at-most-once each of embedSig , KeepActiveToken, eventDM or
            // eventStream
            // KeepActiveToken and embedSig must not both be present
            secondEventParameter = ( embedSig | Tokens::KeepActiveToken | eventDM |
                                     eventStream | eventOther );
    
            embedSig  = Tokens::EmbedToken >> LBRKT >> signalsDescriptor >> RBRKT;
    
            eventStream          = Tokens::StreamToken >> EQUAL >> StreamID;
    
    
            eventOther           = eventParameterName >> parmValue;
    
            eventParameterName   = NAME.alias();
    
            eventDM              = Tokens::DigitMapToken >> EQUAL >> ( digitMapName  |
                                   (LBRKT >> digitMapValue >> RBRKT ));
    
            signalsDescriptor    = Tokens::SignalsToken >> LBRKT >> -( signalParm % COMMA) >> RBRKT;
    
            signalParm           = signalList | signalRequest;
    
            signalRequest        = signalName >> -( LBRKT >> (sigParameter % COMMA) >> RBRKT );
    
            signalList           = Tokens::SignalListToken >> EQUAL >> signalListId >> LBRKT
                                   >> (signalListParm % COMMA) >> RBRKT;
    
            signalListId         = UINT16;
    
            //exactly once signalType, at most once duration and every signal
            //parameter
            signalListParm       = signalRequest.alias();
    
            signalName           = pkgdName.alias();
            //at-most-once sigStream, at-most-once sigSignalType,
            //at-most-once sigDuration, every signalParameterName at most once
            sigParameter = sigStream | sigSignalType | sigDuration | sigOther
                        | notifyCompletion | Tokens::KeepActiveToken;
            sigStream            = Tokens::StreamToken >> EQUAL >> StreamID;
            sigOther             = sigParameterName >> parmValue;
            sigParameterName     = NAME.alias();
            sigSignalType        = Tokens::SignalTypeToken >> EQUAL >> signalType;
            signalType           = (Tokens::OnOffToken | Tokens::TimeOutToken | Tokens::BriefToken);
            sigDuration          = Tokens::DurationToken >> EQUAL >> UINT16;
            notifyCompletion     = Tokens::NotifyCompletionToken >> EQUAL >> (LBRKT
                     >> (notificationReason % COMMA) >> RBRKT);
    
            notificationReason   = ( Tokens::TimeOutToken | Tokens::InterruptByEventToken
                                 | Tokens::InterruptByNewSignalsDescrToken
                                 | Tokens::OtherReasonToken );
            observedEventsDescriptor = Tokens::ObservedEventsToken >> EQUAL >> RequestID
                               >> LBRKT >> (observedEvent % COMMA) >> RBRKT;
    
            //time per event, because it might be buffered
            observedEvent        = -( TimeStamp >> LWSP >> ':') >> LWSP
                                   >> pkgdName >> -( LBRKT >> (observedEventParameter % COMMA) >> RBRKT );
    
            //at-most-once eventStream, every eventParameterName at most once
            observedEventParameter = eventStream | eventOther;
    
            // For an AuditCapReply with all events, the RequestID should be ALL.
            RequestID            = ( UINT32 | "*" );
    
            modemDescriptor      = Tokens::ModemToken >> (( EQUAL >> modemType) |
                               (LSBRKT >> (modemType % COMMA) >> RSBRKT))
                              >> -( LBRKT >> (propertyParm % COMMA) >> RBRKT );
    
            // at-most-once except for extensionParameter
            modemType            = (Tokens::V32bisToken | Tokens::V22bisToken | Tokens::V18Token |
                                    Tokens::V22Token | Tokens::V32Token | Tokens::V34Token | Tokens::V90Token |
                                  Tokens::V91Token | Tokens::SynchISDNToken | extensionParameter);
    
            digitMapDescriptor  = Tokens::DigitMapToken >> EQUAL
                                 >> ( ( LBRKT >> digitMapValue >> RBRKT ) |
                                 (digitMapName >> -( LBRKT >> digitMapValue >> RBRKT )) );
            digitMapName        = NAME.alias();
            digitMapValue       = -("T:" >> Timer >> COMMA) >> -("S:" >> Timer >> COMMA)
                               >> -("L:" >> Timer >> COMMA) >> digitMap;
            Timer               = repeat(1,2)[digit];
            // Units are seconds for T, S, and L timers, and hundreds of
            // milliseconds for Z timer.  Thus T, S, and L range from 1 to 99
            // seconds and Z from 100 ms to 9.9 s
            digitMap = (digitString |
                        LPAREN >> digitStringList >> RPAREN);
            digitStringList   = digitString >> *( PIPE >> digitString );
            digitString       = +digitStringElement;
            digitStringElement = digitPosition >> -lit('.');
            digitPosition     = digitMapLetter | digitMapRange;
            digitMapRange     = ("x" | (LSBRKT >> digitLetter >> RSBRKT));
            digitLetter       = *((digit >> "-" >> digit ) | digitMapLetter);
            digitMapLetter    = char_("0-9" //Basic event symbols
                                      "a-kA-K"
                                      "LS" // Inter-event timers (long, short)
                                      "Z" //Long duration modifier
                                      );
    
            //at-most-once, and DigitMapToken and PackagesToken are not allowed
            //in AuditCapabilities command
            auditItem            = ( Tokens::MuxToken | Tokens::ModemToken | Tokens::MediaToken |
                                    Tokens::SignalsToken | Tokens::EventBufferToken |
                                    Tokens::DigitMapToken | Tokens::StatsToken | Tokens::EventsToken |
                                    Tokens::ObservedEventsToken | Tokens::PackagesToken );
    
    
    
            serviceChangeDescriptor = Tokens::ServicesToken 
                >> LBRKT >> serviceChangeParm >> *(COMMA >> serviceChangeParm) >> RBRKT;
    
            // each parameter at-most-once
            // at most one of either serviceChangeAddress or serviceChangeMgcId
            // but not both
            // serviceChangeMethod and serviceChangeReason are REQUIRED
            serviceChangeParm    = (serviceChangeMethod | serviceChangeReason |
                                   serviceChangeDelay | serviceChangeAddress |
                                   serviceChangeProfile | extension | TimeStamp |
                                   serviceChangeMgcId | serviceChangeVersion );
    
            serviceChangeReplyDescriptor = Tokens::ServicesToken >> LBRKT
                                 >> (servChgReplyParm % COMMA) >> RBRKT;
    
            // at-most-once.  Version is REQUIRED on first ServiceChange response
            // at most one of either serviceChangeAddress or serviceChangeMgcId
            // but not both
            servChgReplyParm     = (serviceChangeAddress | serviceChangeMgcId |
                                   serviceChangeProfile | serviceChangeVersion |
                                   TimeStamp);
            serviceChangeMethod  = Tokens::MethodToken >> EQUAL >> (Tokens::FailoverToken |
                                   Tokens::ForcedToken | Tokens::GracefulToken | Tokens::RestartToken |
                                   Tokens::DisconnectedToken | Tokens::HandOffToken |
                                   extensionParameter);
            // A serviceChangeReason consists of a numeric reason code
            // and an optional text description.
            // A serviceChangeReason MUST be encoded using the quotedString
            // form of VALUE.
            // The quotedString SHALL contain a decimal reason code,
            // optionally followed by a single space character and a
            // textual description string.
    
    
            serviceChangeReason  = Tokens::ReasonToken  >> EQUAL >> VALUE;
            serviceChangeDelay   = Tokens::DelayToken   >> EQUAL >> UINT32;
            serviceChangeAddress = Tokens::ServiceChangeAddressToken >> EQUAL >> ( mId |
                                   portNumber );
            serviceChangeMgcId   = Tokens::MgcIdToken   >> EQUAL >> mId;
            serviceChangeProfile = Tokens::ProfileToken >> EQUAL >> NAME >> '/' >> Version;
            serviceChangeVersion = Tokens::VersionToken >> EQUAL >> Version;
            extension            = extensionParameter >> parmValue;
    
            packagesDescriptor   = Tokens::PackagesToken 
                >> LBRKT >> packagesItem >> *(COMMA >> packagesItem) >> RBRKT;
    
            packagesItem         = NAME >> "-" >> UINT16;
    
    
            TimeStamp            = Date >> "T" >> Time; // per ISO 8601:1988
            // Date = yyyymmdd
            Date                 = repeat(8)[digit];
            // Time = hhmmssss
            Time                 = repeat(8)[digit];
            statisticsDescriptor = Tokens::StatsToken 
                >> LBRKT >> statisticsParameter >> *(COMMA >> statisticsParameter ) >> RBRKT;
    
            //at-most-once per item
            statisticsParameter  = pkgdName >> -(EQUAL >> VALUE);
    
            topologyDescriptor   = Tokens::TopologyToken 
                >> LBRKT >> topologyTriple >> *(COMMA >> topologyTriple) >> RBRKT;
            topologyTriple       = terminationA >> COMMA >>
                                   terminationB >> COMMA >> topologyDirection;
            terminationA         = TerminationID.alias();
            terminationB         = TerminationID.alias();
            topologyDirection    = Tokens::BothwayToken | Tokens::IsolateToken | Tokens::OnewayToken;
    
            priority             = Tokens::PriorityToken >> EQUAL >> UINT16;
    
            extensionParameter   = "X" >> char_("-+") >> repeat(1,6)[alnum];
    
            BOOST_SPIRIT_DEBUG_NODES(
                // snipped for Stack Overflow, see Coliru
            )
    
        }
      private:
        qi::rule<It> megacoMessage, message;
        qi::rule<It, uint32_t()> mtpAddress, ContextID;
    
        qi::rule<It, std::string()> mId, domainName, deviceName, pathNAME, domainAddress, pathDomainName,
            IPv4address, IPv6address, hexpart, hexseq;
        qi::rule<It, uint16_t()> portNumber;
        qi::rule<It, uint8_t()> V4hex;
    
        // implicit lexemes (no implicit whitespace allowed):
        qi::rule<It> authenticationHeader;
        qi::uint_parser<int, 10, 1, 2> Version;
        qi::uint_parser<uint32_t, 10, 1, 10> UINT32;
        qi::uint_parser<uint16_t, 10, 1, 5>  UINT16;
    
        // message payload
        qi::rule<It> messageBody,
            transactionList, transactionPending, transactionResponseAck, transactionAck, transactionRequest,
            actionRequest,
            contextRequest, contextProperties, contextProperty, contextAudit, contextAuditProperties,
            commandRequestList, commandRequest,
            transactionReply,
            actionReplyList, actionReply, commandReply,
            commandReplyList, commandReplys,
            ammRequest, ammParameter, ammsReply,
            subtractRequest,
            auditRequest, auditReply, auditOther,
            terminationAudit,
            contextTerminationAudit,
            auditReturnParameter, auditDescriptor, notifyRequest,
            notifyReply,
            serviceChangeRequest, serviceChangeReply,
            errorDescriptor, ErrorCode, transactionID;
    
        // OTHER STUFF, DESCRIPTORS
        qi::rule<It> 
            terminationIDList,
            TerminationID, mediaDescriptor,
            mediaParm, streamParm,
            streamDescriptor, localControlDescriptor, localParm,
            reservedValueMode, reservedGroupMode,
            streamMode, streamModes,
            propertyParm, parmValue, alternativeValue,
            localDescriptor, remoteDescriptor,
            eventBufferDescriptor, eventSpec, eventSpecParameter, eventBufferControl,
            terminationStateDescriptor, terminationStateParm,
            serviceStates,
            muxDescriptor, MuxType,
            StreamID,
            pkgdName, PackageName,
            ItemID,
            eventsDescriptor, requestedEvent, eventParameter,
            embedWithSig, embedNoSig, embedFirst,
            secondRequestedEvent, secondEventParameter,
            embedSig,
            eventStream, eventOther, eventParameterName, eventDM,
            signalsDescriptor, signalParm, signalRequest, signalList, signalListId, signalListParm, signalName,
            sigParameter, sigStream, sigOther, sigParameterName, sigSignalType, signalType, sigDuration,
            notifyCompletion, notificationReason,
            observedEventsDescriptor, observedEvent, observedEventParameter,
            RequestID,
            modemDescriptor, modemType,
            digitMapDescriptor, digitMapName, digitMapValue,
            Timer,
            digitMap, digitStringList, digitString, digitStringElement, digitPosition, digitMapRange, digitLetter, digitMapLetter,
            auditItem,
            serviceChangeDescriptor, serviceChangeParm, serviceChangeReplyDescriptor,
            servChgReplyParm,
            serviceChangeMethod, serviceChangeReason, serviceChangeDelay, serviceChangeAddress, serviceChangeMgcId, serviceChangeProfile, serviceChangeVersion,
            extension,
            packagesDescriptor,
            packagesItem,
            TimeStamp, Date, Time,
            statisticsDescriptor, statisticsParameter,
            topologyDescriptor, topologyTriple,
            terminationA, terminationB,
            topologyDirection,
            priority,
            extensionParameter;
    };
    
    };
    int main() {
        std::string const  sample = R"(
    !/3 [15.232.33.21]:2134
    T=173619123
    {
        C=230234621
        {
            PR=9,
            MF=ip/187/6/23045241
            {
                MD=V90
            },
            MF=ip/187/6/23045242
            {
                MD=V90
            }
        }
    })";
    
        using It = std::string::const_iterator;
        Megaco<It>::Parser parser;
    
        It f = sample.begin(), l = sample.end();
        bool ok = qi::parse(f, l, parser);
    
        if (ok)
            std::cout << "Parse success\n";
        else
            std::cout << "Parse failed\n";
    
        if (f != l)
            std::cout << "Remaining unparsed input: '" << std::string(f,l) << "'\n";
    }
    

    Prints

    Parse success
    

    ¹ i.e. mostly dumb transformation of the productions, specifically barely no attribute handling and very heavy (e.g. no symbols<> for the modemtypes etc.)