Search code examples
c++dependency-injectionc++17boost-spirit-x3

Dependency injection of parsers using X3


Sometimes I have tight coupling / circular dependencies between parsers. I might have something like this:

parser.hpp

#pragma once

namespace parser {
    using a_type = x3::rule<class a_class>;
    a_type const a = "a";

    using b_type = x3::rule<class b_class>;
    b_type const b = "b";

    auto const a_def = "(" >> b >> ")";
    auto const b_def = "<" >> a >> ">";

    BOOST_SPIRIT_DEFINE(a, b);
}

However, I want to separate these into different headers, as that will make for smaller compilation times when I unit test and for having multiple header files (rather than one monolithic file). In reality, I have much more than just 2 parsers.

I want to be able to do something like this:

a.hpp

#pragma once

#include "b.hpp"

namespace parser {
    using a_type = x3::rule<class a_class>;
    a_type const a = "a";
    auto const a_def = "(" >> b_wrapper<a>::b >> ")";

    BOOST_SPIRIT_DEFINE(a);
}

b.hpp

#pragma once

namespace parser {
    template <auto Injected>
    struct b_wrapper {
        using b_type = x3::rule<class b_class>;
        static b_type const b = "b";
        static auto const b_def = "(" >> Injected >> ")";

        BOOST_SPIRIT_DEFINE(b);
    };
}

Naturally, this doesn't work. How can I achieve dependency injection with the parsers?


I don't care about the exact syntax for the injection. However, I really need these points:

  • Able to test a.hpp and b.hpp without including both headers in both test-files (a could easily be made to have its dependency injected too)
  • Able to substitute the injected parser with another parser to ease testing

I'm okay if I have to have a bunch of boilerplate every time I inject a different parser.


Solution

  • You can produce your foo_def objects via functions. That is, b.hpp would look like this:

    #pragma once
    
    namespace parser {
        template <typename Injected>
        auto b_impl(Injected const& injected) {
            return "(" >> injected >> ")";
        }
    }
    

    And where you want to inject the actual parser, you can do:

    using b_type = x3::rule<class b_class>;
    b_type const b = "b";
    auto const b_def = b_impl(a);
    
    BOOST_SPIRIT_DEFINE(b);
    

    Note that there is still that bit of boilerplate when you want to create the parser.