Boost.Spirit,或将“ Spirituality”添加到列表过滤器

图片


大家好 我仍然是ISPsystem开发人员,我的名字仍然是Dmitry Smirnov。 在过去(相当长的一段时间)里,我无法决定下一个出版物的主题,因为在过去几个月与Boost.Asio合作的过程中积累了很多材料。 甚至在抛硬币似乎更容易的那一刻,一项任务也改变了一切。 有必要开发一种工具,使前端可以过滤请求列表中的数据。 来自后端的列表本身是一个普通的json_array。 欢迎来到凯特,最近几天都有跌宕起伏。


免责声明


我必须马上说,作者上一次“感觉”十年前的上下文无关语法。 那时,它似乎是一种相当含糊且不必要的工具,但在解决问题的那一天,我就了解了Boost.Spirit库。


挑战赛


需要进行如下查询:


(string_field CP value AND int_field NOT LT 150) OR bool_field EQ true 

进入某种结构,该结构将检查json对象并报告其是否满足要求。


第一步


首先,您需要确定未来过滤器的接口。 必须从数组中删除不必要的对象,因此必须将其与STL算法结合使用,尤其是std :: remove_if。


函子将是完美的,它将直接根据前端的请求进行构建。 由于该项目使用nlohmann :: json,因此设计将非常优雅:


 filter = "(string_field CP value AND int_field NOT LT 150) OR bool_field EQ true"; json.erase(std::remove_if(json.begin(), json.end(), std::not_fn(JsonFilter{filter})), json.end()); 

为了方便使用过滤器,我选择将条件拆分为二叉树。 最低顶点应包含比较运算符,但其余顶点应为逻辑运算符。 这是上面的过滤器在爆炸状态下的外观:


筛选树


结果是AST的一种形式,如果可以这样称呼的话。 既然即将出现的逻辑图景已经成形,那么最有趣,最可怕的时刻已经来临。 这必须写在...关于精神...


熟人


最困难的问题出现了:从哪里开始? 与Asio不同,阅读Spirit标头并没有给出任何明确的线索,换句话说,存在“某种魔术”。 随后,对boost官方文档中的示例进行了研究,并研究了网络上的各种示例,经过一段时间后,它们不仅取得了成果,而且使解决方案尽可能接近我的需求: AST计算器


让我们看一下示例中显示的语法:


语法计算器
 class ArithmeticGrammar1 : public qi::grammar<std::string::const_iterator, ASTNodePtr(), qi::space_type> { public: using Iterator = std::string::const_iterator; ArithmeticGrammar1() : ArithmeticGrammar1::base_type(start) { start = (product >> '+' >> start) [qi::_val = phx::new_<OperatorNode<'+'>> (qi::_1, qi::_2)] | product[qi::_val = qi::_1]; product = (factor >> '*' >> product) [qi::_val = phx::new_<OperatorNode<'*'>> (qi::_1, qi::_2)] | factor[qi::_val = qi::_1]; factor = group[qi::_val = qi::_1] | qi::int_[qi::_val = phx::new_<ConstantNode>(qi::_1)]; group %= '(' >> start >> ')'; } qi::rule<Iterator, ASTNodePtr(), qi::space_type> start, group, product, factor; }; 

语法是从基本qi ::语法继承的。 ASTNodePtr()不是一个显而易见的方法,但是将预期结果的对象传递到语法对象中的方法非常方便。


AST节点计算器
 class ASTNode { public: virtual double evaluate() = 0; virtual ~ASTNode() {} }; using ASTNodePtr = ASTNode*; template <char Operator> class OperatorNode : public ASTNode { public: OperatorNode(const ASTNodePtr &left, const ASTNodePtr &right) : left(left) , right(right) {} double evaluate() { if (Operator == '+') return left->evaluate() + right->evaluate(); else if (Operator == '*') return left->evaluate() * right->evaluate(); } ~OperatorNode() { delete left; delete right; } private: ASTNodePtr left, right; //  }; class ConstantNode : public ASTNode { public: ConstantNode(double value) : value(value) {} double evaluate() { return value; } private: double value; }; 

使用Boost.Phoenix库,您可以在解析期间从一个或多个非终端创建一个现成的AST节点,并直接写入结果。 让我们仔细看一下计算器的组成:


 start = (product >> '+' >> start)[qi::_val = phx::new_<OperatorNode<'+'>> (qi::_1, qi::_2)] | product[qi::_val = qi::_1]; 

start-开始分析句子。 起点。 它可以通过乘积与开始之和表示,也可以通过乘积来表示。


注意每个表达式的方括号中的操作。 如果解析成功,并且所有内容都匹配,则应执行此操作。 qi :: _ val实际上是增强::精神:: qi :: _ val是一个占位符。 在其帮助下,答案将记录在结果中。 在启动的情况下,这将是一个OperatorNode对象,其第一个参数将是解析乘积的结果,第二个参数是解析开始的结果。


我们进一步看。 假设我们遇到第二种选择,开始不是一个和,而是一个乘积。 他如何表达?


 product = (factor >> '*' >> product) [qi::_val = phx::new_<OperatorNode<'*'>> (qi::_1, qi::_2)] | factor[qi::_val = qi::_1]; 

重复前一张图片,差异最小。 再次遇到某种表达式,再次在结果中写入OperatorNode对象,或者只是某种因数。 让我们看看。


 factor = group[qi::_val = qi::_1] | qi::int_[qi::_val = phx::new_<ConstantNode>(qi::_1)]; 

当我们沿着最短的路径前进时,我们假设遇到了int。 现在,如果我们用伪代码描述前面的所有步骤,我们将得到如下扩展形式:


 factor1 = ConstantNode(1) //   ,    factor2 = ConstantNode(3) product = OperatorNode<'*'>(factor1, factor2) start = product 

从顶部开始的每个节点(最低的节点除外,此处本质上是整数)通过后续节点表示。 唯一调用根元素上的validate ()方法解决了整个问题,太好了!


然后qi :: space_type引起您的注意-此参数是解析时忽略的元素列表。 这仍然会在我身上发挥作用:-)。


此处引人注目的是通过简单地通过乘积(*)表示非结束起始(仅包含+)来优先于乘法进行加法的方法。 在我的语法变体中,由于确定“与”将优先于“或”,因此我只在正确的位置替换所需的逻辑运算符。 如果很难在编写数学运算符时犯错误,那么文本逻辑运算符就是一个完全不同的故事。 希望解决至少部分可能的问题,例如寄存器。 为此,Spirit具有内置类型qi :: no_case


此外,除了数字以外,我还需要这些字段的名称,因此我们添加了相应的非终结符,而不是内置的qi :: int_ spirit:


 field = qi::char_("a-zA-Z_") >> *qi::char_("a-zA-Z_0-9"); 

我们在这里得到了这样一个简单的表达式(到目前为止还没有语义运算):


 start = product >> qi::no_case["OR"] >> start | product; product = factor >> qi::no_case["AND"] >> product | factor; factor = group | field; group %= '(' >> start >> ')'; 

现在,一切就绪,可以解析最简单的句子“ field And field2” 。 我们开始了,……什么都没有。


问题出在一个意想不到的地方: qi :: space_type不仅会忽略空格, 还会在解析之前将其从句子中删除,并且初始过滤器表达式已经以以下形式解析:


 "fieldAndfield2" \\        ,    "(5 * 5) + 11 " \\  "(5*5)+11" 

这仅仅是一个领域。 因此,您需要一些队长:


 skipper = +qi::lit(' '); //    . ,    ,   ,  ,  C++ . start = product >> skipper >> qi::no_case["OR"] >> skipper >> start | product; ... 

在分析完字段之后,就可以学习如何从表达式中获取值,并了解如何根据该值对字段进行验证。 可以通过以下操作来表示所有比较选项:


 enum class Operator { EQ, //  LT, //  GT, //  CP //  (  ) }; unary = qi::no_case["NOT"]; // ,          

值本身以这样的非终结符表示:


 value = qi::double_ | qi::int_ | qi::bool_ | string; string = qi::lit("'") >> +qi::char_("a-zA-Z0-9_. ") >> qi::lit("'"); 

现在谈到这种获取价值的方法所带来的问题。 Spirit将以boost :: variant <int,double,bool,std :: string>的形式返回它,并且当需要将其与某些数据进行比较时,将需要一些技巧来获取所需类型的值。 这是我来到的选项:


 using ValueType = boost::variant<int, double, bool, std::string>; struct ValueGetter : public boost::static_visitor<Json> { template <typename Type> Json operator()(const Type &value) const { return value; } }; 

为什么吸气剂会返回Json对象? 因此,当在过滤过程中比较值时,我将避免必须弄清楚比较所经历的数据类型,而将所有工作留给json库。


终点线。 匹配者本人的描述。 我们将使用与计算器相同的示例。 首先,我们需要一个抽象,并将其传递到语法中,Spirit会用我们将其填充:


 class AbstractMatcher { public: AbstractMatcher() = default; virtual ~AbstractMatcher() = default; virtual bool evaluate(const Json &object) = 0; //       }; using MatcherPtr = std::shared_ptr<AbstractMatcher>; 

其他逻辑节点是主要过滤器节点:


逻辑节点
 enum class Logic { AND, OR }; template <Logic Operator> class LogicNode final : public AbstractMatcher { public: LogicNode(MatcherPtr &left, MatcherPtr &right) : m_left(std::move(left)) , m_right(std::move(right)) { switch (Operator) { case Logic::AND: m_evaluator = &LogicNode::And; break; case Logic::OR: m_evaluator = &LogicNode::Or; } } bool evaluate(const Json &object) final { return std::invoke(m_evaluator, this, object); } private: MatcherPtr m_left; MatcherPtr m_right; using EvaluateType = bool(LogicNode::*)(const Json &); EvaluateType m_evaluator = nullptr; bool And(const Json &object) { return m_left->evaluate(object) && m_right->evaluate(object); } bool Or(const Json &object) { return m_left->evaluate(object) || m_right->evaluate(object); } }; 

最后,最底层的节点


价值比较
 class ObjectNode final : public AbstractMatcher { public: ObjectNode(std::string field, const ValueType &value, boost::optional<std::string> &unary, Operator oper) : m_field(std::move(field)) , m_json_value(boost::apply_visitor(ValueGetter(), value)) , m_reject(unary.has_value()) { switch (oper) { case Operator::EQ: m_evaluator = &ObjectNode::Equal; break; case Operator::LT: m_evaluator = &ObjectNode::LesserThan; break; case Operator::GT: m_evaluator = &ObjectNode::GreaterThan; break; case Operator::CP: m_evaluator = &ObjectNode::Substr; break; } } bool evaluate(const Json &object) final { const auto &value = object.at(m_field); const bool result = std::invoke(m_evaluator, this, value); return m_reject ? !result : result; } private: using EvaluateType = bool(ObjectNode::*)(const Json &); const std::string m_field; const Json m_json_value; const bool m_reject; EvaluateType m_evaluator = nullptr; bool Equal(const Json &json) { return json == m_json_value; } bool LesserThan(const Json &json) { return json < m_json_value; } bool GreaterThan(const Json &json) { return json > m_json_value; } bool Substr(const Json &json) { return Str(json).find(Str(m_json_value)) != std::string::npos; } }; 

剩下的只是将它们放在一起:


杰森过滤器
 struct JsonFilterGrammar : qi::grammar<std::string::const_iterator, MatcherPtr()> { JsonFilterGrammar() : JsonFilterGrammar::base_type(expression) { skipper = +qi::lit(' '); unary = qi::no_case["NOT"]; compare.add ("eq", Operator::EQ) ("lt", Operator::LT) ("gt", Operator::GT) ("cp", Operator::CP); expression = (product >> skipper >> qi::no_case["OR"] >> skipper >> expression) [qi::_val = make_shared_<LogicNode<Logic::OR>>()(qi::_1, qi::_2)] | product[qi::_val = qi::_1]; product = (term >> skipper >> qi::no_case["AND"] >> skipper >> product) [qi::_val = make_shared_<LogicNode<Logic::AND>>()(qi::_1, qi::_2)]| term[qi::_val = qi::_1]; term = group[qi::_val = qi::_1] | (field >> -(skipper >> unary)>> skipper >> qi::no_case[compare] >> skipper >> value) [qi::_val = make_shared_<ObjectNode>()(qi::_1, qi::_4, qi::_2, qi::_3)]; field = qi::char_("a-zA-Z_") >> *qi::char_("a-zA-Z_0-9"); value = qi::double_ | qi::int_ | qi::bool_ | string; string = qi::lit("'") >> +qi::char_("a-zA-Z0-9_. \u20BD€$¥-") >> qi::lit("'"); group %= '(' >> expression >> ')'; } qi::rule<Iterator> skipper; qi::rule<Iterator, MatcherPtr()> product, term, expression, group; qi::rule<Iterator, std::string()> field, unary, string; qi::rule<Iterator, ValueType()> value; qi::symbols<char, Operator> compare; //      enum }; 

仅此而已。 现在,获取完成的过滤器已成为相当简单的操作:


 MatcherPtr matcher; std::string filter = "int not LT 15"; JsonFilterGrammar grammar; qi::parse(filter.begin(), filter.end(), grammar, matcher); //     matcher   . 

我将省略将语法包装在函子中的过程(我认为这对任何人都不会很有趣)。 我们将使用最简单的示例来更好地考虑实际使用的工具:


 std::string filter = "int not LT 15"; Json json{ {{"int", 10}}, {{"int", 11}}, {{"int", 20}}, {{"int", 30}}, {{"int", 9}} }; std::cout << json.dump() << std::endl; json.erase(std::remove_if(json.begin(), json.end(), std::not_fn(JsonFilter{filter})), json.end()); std::cout << json.dump() << std::endl; 

这是输出:


 [{"int":10},{"int":11},{"int":20},{"int":30},{"int":9}] [{"int":20},{"int":30}] 

亲爱的读者,我希望您也对和我一样了解灵的基本知识感兴趣。 然后我留下来。 待会儿见。

Source: https://habr.com/ru/post/zh-CN472004/


All Articles