| /** | |
| @section license License | |
| Licensed to the Apache Software Foundation (ASF) under one | |
| or more contributor license agreements. See the NOTICE file | |
| distributed with this work for additional information | |
| regarding copyright ownership. The ASF licenses this file | |
| to you under the Apache License, Version 2.0 (the | |
| "License"); you may not use this file except in compliance | |
| with the License. You may obtain a copy of the License at | |
| http://www.apache.org/licenses/LICENSE-2.0 | |
| Unless required by applicable law or agreed to in writing, software | |
| distributed under the License is distributed on an "AS IS" BASIS, | |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| See the License for the specific language governing permissions and | |
| limitations under the License. | |
| Copyright 2019, Oath Inc. | |
| */ | |
| #include "txn_box/Modifier.h" | |
| #include "txn_box/Context.h" | |
| #include "txn_box/Config.h" | |
| #include "txn_box/Comparison.h" | |
| #include "txn_box/yaml_util.h" | |
| #include "tsutil/LocalBuffer.h" | |
| using swoc::Errata; | |
| using swoc::Rv; | |
| using swoc::TextView; | |
| using namespace swoc::literals; | |
| static constexpr size_t FILTER_LIST_LOCAL_BUFFER_SIZE = 32; | |
| Errata | |
| Modifier::define(swoc::TextView name, Modifier::Worker const &f) | |
| { | |
| if (auto spot = _factory.find(name); spot == _factory.end()) { | |
| _factory.insert(spot, {name, f}); | |
| return {}; | |
| } | |
| return Errata(S_ERROR, R"(Modifier "{}" is already defined.)", name); | |
| } | |
| Rv<Modifier::Handle> | |
| Modifier::load(Config &cfg, YAML::Node const &node, ActiveType ex_type) | |
| { | |
| if (!node.IsMap()) { | |
| return Errata(S_ERROR, R"(Modifier at {} is not an object as required.)", node.Mark()); | |
| } | |
| for (auto const &[key_node, value_node] : node) { | |
| TextView key{key_node.Scalar()}; | |
| auto &&[arg, arg_errata]{parse_arg(key)}; | |
| if (!arg_errata.is_ok()) { | |
| return std::move(arg_errata); | |
| } | |
| // See if @a key is in the factory. | |
| if (auto spot{_factory.find(key)}; spot != _factory.end()) { | |
| auto &&[handle, errata]{spot->second(cfg, node, key, arg, value_node)}; | |
| if (!errata.is_ok()) { | |
| return std::move(errata); | |
| } | |
| if (!handle->is_valid_for(ex_type)) { | |
| return Errata(S_ERROR, R"(Modifier "{}" at {} cannot accept a feature of type "{}".)", key, node.Mark(), ex_type); | |
| } | |
| return std::move(handle); | |
| } | |
| } | |
| return Errata(S_ERROR, R"(No valid modifier key in object at {}.)", node.Mark()); | |
| } | |
| swoc::Rv<Feature> | |
| Modifier::operator()(Context &, feature_type_for<NIL>) | |
| { | |
| return NIL_FEATURE; | |
| } | |
| swoc::Rv<Feature> | |
| Modifier::operator()(Context &, feature_type_for<IP_ADDR>) | |
| { | |
| return NIL_FEATURE; | |
| } | |
| swoc::Rv<Feature> | |
| Modifier::operator()(Context &, feature_type_for<STRING>) | |
| { | |
| return NIL_FEATURE; | |
| } | |
| // --- | |
| class Mod_hash : public Modifier | |
| { | |
| using self_type = Mod_hash; | |
| using super_type = Modifier; | |
| public: | |
| static const std::string KEY; ///< Identifier name. | |
| using Modifier::operator(); // declare hidden member function | |
| /** Modify the feature. | |
| * | |
| * @param ctx Run time context. | |
| * @param feature Feature to modify. | |
| * @return Errors, if any. | |
| */ | |
| Rv<Feature> operator()(Context &ctx, feature_type_for<STRING> feature) override; | |
| /** Check if @a ftype is a valid type to be modified. | |
| * | |
| * @param ex_type Type of feature to modify. | |
| * @return @c true if this modifier can modity that feature type, @c false if not. | |
| */ | |
| bool is_valid_for(ActiveType const &ex_type) const override; | |
| /// Resulting type of feature after modifying. | |
| ActiveType result_type(ActiveType const &) const override; | |
| /** Create an instance from YAML config. | |
| * | |
| * @param cfg Configuration state object. | |
| * @param mod_node Node with modifier. | |
| * @param key_node Node in @a mod_node that identifies the modifier. | |
| * @return A constructed instance or errors. | |
| */ | |
| static Rv<Handle> load(Config &cfg, YAML::Node node, TextView key, TextView arg, YAML::Node key_value); | |
| protected: | |
| unsigned _n = 0; ///< Number of hash buckets. | |
| /// Constructor for @c load. | |
| Mod_hash(unsigned n); | |
| }; | |
| const std::string Mod_hash::KEY{"hash"}; | |
| Mod_hash::Mod_hash(unsigned n) : _n(n) {} | |
| bool | |
| Mod_hash::is_valid_for(ActiveType const &ex_type) const | |
| { | |
| return ex_type.can_satisfy(STRING); | |
| } | |
| ActiveType | |
| Mod_hash::result_type(ActiveType const &) const | |
| { | |
| return {NIL, INTEGER}; | |
| } | |
| Rv<Feature> | |
| Mod_hash::operator()(Context &, feature_type_for<STRING> feature) | |
| { | |
| feature_type_for<INTEGER> value = std::hash<std::string_view>{}(feature); | |
| return Feature{feature_type_for<INTEGER>{value % _n}}; | |
| } | |
| Rv<Modifier::Handle> | |
| Mod_hash::load(Config &, YAML::Node node, TextView, TextView, YAML::Node key_value) | |
| { | |
| if (!key_value.IsScalar()) { | |
| return Errata(S_ERROR, R"(Value for "{}" at {} in modifier at {} is not a number as required.)", KEY, key_value.Mark(), | |
| node.Mark()); | |
| } | |
| TextView src{key_value.Scalar()}, parsed; | |
| src.trim_if(&isspace); | |
| auto n = swoc::svtou(src, &parsed); | |
| if (src.size() != parsed.size()) { | |
| return Errata(S_ERROR, R"(Value "{}" for "{}" at {} in modifier at {} is not a number as required.)", src, KEY, | |
| key_value.Mark(), node.Mark()); | |
| } | |
| if (n < 2) { | |
| return Errata(S_ERROR, R"(Value "{}" for "{}" at {} in modifier at {} must be at least 2.)", src, KEY, key_value.Mark(), | |
| node.Mark()); | |
| } | |
| return {Handle{new self_type(n)}, {}}; | |
| } | |
| // --- | |
| /// Do replacement based on regular expression matching. | |
| class Mod_rxp_replace : public Modifier | |
| { | |
| using self_type = Mod_rxp_replace; | |
| using super_type = Modifier; | |
| public: | |
| static inline const std::string KEY{"rxp-replace"}; ///< Identifier name. | |
| using Modifier::operator(); // declare hidden member function | |
| static Rv<Handle> load(Config &cfg, YAML::Node node, TextView key, TextView arg, YAML::Node key_value); | |
| bool is_valid_for(ActiveType const &ex_type) const override; | |
| ActiveType result_type(ActiveType const &) const override; | |
| Rv<Feature> operator()(Context &ctx, feature_type_for<STRING> src) override; | |
| static constexpr TextView ARG_GLOBAL = "g"; ///< Global replace. | |
| static constexpr TextView ARG_NOCASE = "nc"; ///< Case insensitive match. | |
| protected: | |
| RxpOp _op; ///< Regular expression operator. | |
| Expr _replacement; ///< Replacement text. | |
| bool _global_p = false; | |
| Mod_rxp_replace(RxpOp &&op, Expr &&replacement) : _op(std::move(op)), _replacement(std::move(replacement)) {} | |
| }; | |
| bool | |
| Mod_rxp_replace::is_valid_for(ActiveType const &ex_type) const | |
| { | |
| return ex_type.can_satisfy(STRING); | |
| } | |
| ActiveType | |
| Mod_rxp_replace::result_type(ActiveType const &) const | |
| { | |
| return {NIL, STRING}; | |
| } | |
| Rv<Modifier::Handle> | |
| Mod_rxp_replace::load(Config &cfg, YAML::Node node, TextView, TextView args, YAML::Node key_value) | |
| { | |
| Rxp::Options opt; | |
| bool global_p = false; | |
| if (!key_value.IsSequence() || key_value.size() != 2) { | |
| return Errata(S_ERROR, R"(Value for modifier "{}" at {} is not list of size 2 - [ pattern, replacement ] - as required.)", KEY, | |
| node.Mark()); | |
| } | |
| while (args) { | |
| auto token = args.take_prefix_at(','); | |
| if (ARG_GLOBAL == token) { | |
| global_p = true; | |
| } else if (ARG_NOCASE == token) { | |
| opt.f.nc = true; | |
| } else { | |
| return Errata(S_ERROR, R"(Invalid option "{}" for modifier "{}" at {}.)", token, KEY, key_value.Mark()); | |
| } | |
| } | |
| YAML::Node rxp_src_node = key_value[0]; | |
| auto &&[pattern, pattern_errata]{cfg.parse_expr(key_value[0])}; | |
| if (!pattern_errata.is_ok()) { | |
| pattern_errata.note(R"(While parsing expression for "{}" modifier at {}.)", KEY, key_value.Mark()); | |
| return std::move(pattern_errata); | |
| } | |
| auto &&[op, op_errata]{RxpOp::load(cfg, std::move(pattern), opt)}; | |
| if (!op_errata.is_ok()) { | |
| op_errata.note(R"(While parsing pattern for modifier "{}".)", KEY); | |
| return std::move(op_errata); | |
| } | |
| cfg.require_rxp_group_count(op.capture_count()); | |
| auto &&[rep, rep_errata]{cfg.parse_expr(key_value[1])}; | |
| if (!rep_errata.is_ok()) { | |
| rep_errata.note(R"(While parsing replacement for modifier "{}".)", KEY); | |
| return std::move(rep_errata); | |
| } | |
| auto self = new self_type(std::move(op), std::move(rep)); | |
| self->_global_p = global_p; | |
| return {Handle(self)}; | |
| } | |
| Rv<Feature> | |
| Mod_rxp_replace::operator()(Context &ctx, feature_type_for<STRING> src) | |
| { | |
| /* Some unfortunate issues make this more complex than one might assume. | |
| * The root issue is the non-recursive nature of the transient buffer. To generate the | |
| * replacement text the feature for it must be extracted every time, because it is likely to | |
| * depend on extracting capture groups which can vary with every replacement. This will trash | |
| * the overall result if it also stored in the transient buffer. Therefore it is necessary to | |
| * construct pieces of the final string as the replacements are done, and then assemble them | |
| * later. At some point it could be desirable to make this sort of piece wise string a general | |
| * type that can be passed around, without having to immediately render it as a contiguous string. | |
| * For now, this is just here. | |
| */ | |
| struct piece { | |
| TextView _text; | |
| piece *_next = nullptr; | |
| }; | |
| piece anchor; // to avoid checking for nullptr / start of list. | |
| piece *last = &anchor; // last piece added. | |
| [[maybe_unused]] int result; | |
| while (src && (result = _op(ctx, src)) > 0) { | |
| auto match = ctx.active_group(0); | |
| auto p = ctx.make<piece>(); | |
| p->_text.assign(src.data(), match.data()); | |
| last->_next = p; | |
| last = p; | |
| // write the replacement text. | |
| auto r = ctx.extract(_replacement); | |
| if (r.index() == IndexFor(STRING)) { | |
| ctx.commit(r); | |
| auto rp = ctx.make<piece>(); | |
| rp->_text = std::get<IndexFor(STRING)>(r); | |
| last->_next = rp; | |
| last = rp; | |
| } | |
| // Clip the match. | |
| src.assign(match.data_end(), src.data_end()); | |
| if (!_global_p) { | |
| break; | |
| } | |
| } | |
| // How big is the result? | |
| size_t n = src.size(); | |
| for (piece *spot = anchor._next; spot != nullptr; spot = spot->_next) { | |
| n += spot->_text.size(); | |
| } | |
| // Pieces assemble! | |
| auto span = ctx.transient_buffer(n).rebind<char>(); | |
| swoc::FixedBufferWriter w(span); | |
| for (piece *spot = anchor._next; spot != nullptr; spot = spot->_next) { | |
| w.write(spot->_text); | |
| } | |
| w.write(src); | |
| ctx.transient_finalize(w.size()); | |
| return {FeatureView(w.view())}; | |
| } | |
| // --- | |
| /// Filter a list. | |
| class Mod_filter : public FilterMod | |
| { | |
| using self_type = Mod_filter; | |
| using super_type = Modifier; | |
| public: | |
| static inline const std::string KEY = "filter"; ///< Identifier name. | |
| using super_type::operator(); // declare hidden member function | |
| /** Modify the feature. | |
| * | |
| * @param ctx Run time context. | |
| * @param feature Feature to modify [in,out] | |
| * @return Errors, if any. | |
| */ | |
| Rv<Feature> operator()(Context &ctx, Feature &feature) override; | |
| /** Check if @a ftype is a valid type to be modified. | |
| * | |
| * @param ftype Type of feature to modify. | |
| * @return @c true if this modifier can modify that feature type, @c false if not. | |
| */ | |
| bool is_valid_for(ActiveType const &ex_type) const override; | |
| /// Resulting type of feature after modifying. | |
| ActiveType result_type(ActiveType const &ex_type) const override; | |
| /** Create an instance from YAML config. | |
| * | |
| * @param cfg Configuration state object. | |
| * @param mod_node Node with modifier. | |
| * @param key_node Node in @a mod_node that identifies the modifier. | |
| * @return A constructed instance or errors. | |
| */ | |
| static Rv<Handle> load(Config &cfg, YAML::Node node, TextView key, TextView arg, YAML::Node key_value); | |
| /// A filter comparison case. | |
| struct Case { | |
| Action _action = PASS; ///< Action on match. | |
| Expr _expr; ///< Replacement expression, if any. | |
| Comparison::Handle _cmp; ///< Comparison. | |
| /// Assign the comparison for this case. | |
| void assign(Comparison::Handle &&handle); | |
| bool operator()(Context &ctx, Feature const &feature); | |
| /** YAML load hook. | |
| * | |
| * @param cfg Configuration. | |
| * @param node Node containing comparison. | |
| * @return Errors, if any. | |
| * | |
| * This is called during case loading, before the generic loading is done. It is required to | |
| * check any non-generic keys and remove them - anything not expected by the generic load will | |
| * be flagged as an error. | |
| */ | |
| Errata pre_load(Config &cfg, YAML::Node node); | |
| }; | |
| /// Container for cases with comparisons. | |
| ComparisonGroup<Case> _cases; | |
| /** Perform the comparisons for the filter. | |
| * | |
| * @param ctx Txn context | |
| * @param feature Feature to compare | |
| * @return The matched @c Case or @c nullptr if no case matched. | |
| */ | |
| Case const *compare(Context &ctx, Feature const &feature) const; | |
| }; | |
| bool | |
| Mod_filter::is_valid_for(ActiveType const &) const | |
| { | |
| return true; | |
| } | |
| ActiveType | |
| Mod_filter::result_type(ActiveType const &ex_type) const | |
| { | |
| return ex_type; | |
| } | |
| auto | |
| Mod_filter::compare(Context &ctx, Feature const &feature) const -> Case const * | |
| { | |
| for (auto const &c : _cases) { | |
| if (!c._cmp || (*c._cmp)(ctx, feature)) { | |
| return &c; | |
| } | |
| } | |
| return nullptr; | |
| } | |
| Rv<Feature> | |
| Mod_filter::operator()(Context &ctx, Feature &feature) | |
| { | |
| Feature zret{}; | |
| if (feature.is_list()) { | |
| auto src = std::get<IndexFor(TUPLE)>(feature); | |
| ts::LocalBuffer<Feature, FILTER_LIST_LOCAL_BUFFER_SIZE> farray(src.count()); | |
| feature_type_for<TUPLE> dst = {farray.data(), src.count()}; | |
| unsigned dst_idx = 0; | |
| for (Feature f = feature; !is_nil(f); f = cdr(f)) { | |
| Feature item = car(f); | |
| auto c = _cases(ctx, item); | |
| Action action = ((c != _cases.end()) ? c->_action : DROP); | |
| switch (action) { | |
| case DROP: | |
| break; | |
| case PASS: | |
| dst[dst_idx++] = item; | |
| break; | |
| case REPLACE: | |
| dst[dst_idx++] = ctx.extract(c->_expr); | |
| break; | |
| } | |
| } | |
| auto span = ctx.alloc_span<Feature>(dst_idx); | |
| for (unsigned idx = 0; idx < dst_idx; ++idx) { | |
| span[idx] = dst[idx]; | |
| } | |
| zret = span; | |
| } else { | |
| auto c = this->compare(ctx, feature); | |
| Action action = c ? c->_action : DROP; | |
| switch (action) { | |
| case DROP: | |
| zret = NIL_FEATURE; | |
| break; | |
| case PASS: | |
| zret = feature; | |
| break; | |
| case REPLACE: | |
| zret = ctx.extract(c->_expr); | |
| break; | |
| } | |
| } | |
| return zret; | |
| } | |
| void | |
| Mod_filter::Case::assign(Comparison::Handle &&handle) | |
| { | |
| _cmp = std::move(handle); | |
| } | |
| Errata | |
| Mod_filter::Case::pre_load(Config &cfg, YAML::Node cmp_node) | |
| { | |
| if (!cmp_node.IsMap()) { | |
| return Errata(S_ERROR, "List element at {} for {} modifier is not a comparison object.", cmp_node.Mark(), KEY); | |
| } | |
| Expr replace_expr; | |
| unsigned action_count = 0; | |
| if (auto do_node = cmp_node[Global::DO_KEY]; do_node) { | |
| return Errata(S_ERROR, R"("{}" at line {} is not allowed in a modifier comparison.)", Global::DO_KEY, do_node.Mark()); | |
| } | |
| YAML::Node drop_node = cmp_node[ACTION_DROP]; | |
| if (drop_node) { | |
| _action = DROP; | |
| cmp_node.remove(ACTION_DROP); | |
| ++action_count; | |
| } | |
| YAML::Node pass_node = cmp_node[ACTION_PASS]; | |
| if (pass_node) { | |
| _action = PASS; | |
| cmp_node.remove(ACTION_PASS); | |
| ++action_count; | |
| } | |
| YAML::Node replace_node = cmp_node[ACTION_REPLACE]; | |
| if (replace_node) { | |
| auto &&[expr, errata] = cfg.parse_expr(replace_node); | |
| if (!errata.is_ok()) { | |
| errata.note("While parsing expression at {} for {} key in comparison at {}.", replace_node.Mark(), ACTION_REPLACE, | |
| cmp_node.Mark()); | |
| return std::move(errata); | |
| } | |
| _expr = std::move(expr); | |
| _action = REPLACE; | |
| cmp_node.remove(ACTION_REPLACE); | |
| ++action_count; | |
| } | |
| if (action_count > 1) { | |
| return Errata(S_ERROR, "Only one of {}, {}, {} is allowed in the {} comparison at {}.", ACTION_REPLACE, ACTION_DROP, | |
| ACTION_PASS, KEY, cmp_node.Mark()); | |
| } | |
| return {}; | |
| } | |
| bool | |
| Mod_filter::Case::operator()(Context &ctx, Feature const &feature) | |
| { | |
| return !_cmp || (*_cmp)(ctx, feature); | |
| } | |
| Rv<Modifier::Handle> | |
| Mod_filter::load(Config &cfg, YAML::Node node, TextView, TextView, YAML::Node key_value) | |
| { | |
| auto self = new self_type; | |
| Handle handle(self); | |
| auto active_type = cfg.active_type(); | |
| auto scope{cfg.feature_scope(active_type.can_satisfy(TUPLE) ? active_type.tuple_types() : active_type)}; | |
| if (auto errata = self->_cases.load(cfg, key_value); !errata.is_ok()) { | |
| errata.note(R"(While parsing modifier "{}" at line {}.)", KEY, node.Mark()); | |
| return errata; | |
| } | |
| return handle; | |
| } | |
| // --- | |
| /// Replace the feature with another feature if the input is nil or empty. | |
| class Mod_else : public Modifier | |
| { | |
| using self_type = Mod_else; | |
| using super_type = Modifier; | |
| public: | |
| static constexpr TextView KEY{"else"}; ///< Identifier name. | |
| using super_type::operator(); // declare hidden member function | |
| /** Modify the feature. | |
| * | |
| * @param ctx Run time context. | |
| * @param feature Feature to modify [in,out] | |
| * @return Errors, if any. | |
| */ | |
| Rv<Feature> operator()(Context &ctx, Feature &feature) override; | |
| /** Check if @a ftype is a valid type to be modified. | |
| * | |
| * @param ftype Type of feature to modify. | |
| * @return @c true if this modifier can modity that feature type, @c false if not. | |
| */ | |
| bool is_valid_for(ActiveType const &ex_type) const override; | |
| /// Resulting type of feature after modifying. | |
| ActiveType result_type(ActiveType const &) const override; | |
| /** Create an instance from YAML config. | |
| * | |
| * @param cfg Configuration state object. | |
| * @param mod_node Node with modifier. | |
| * @param key_node Node in @a mod_node that identifies the modifier. | |
| * @return A constructed instance or errors. | |
| */ | |
| static Rv<Handle> load(Config &cfg, YAML::Node node, TextView key, TextView arg, YAML::Node key_value); | |
| protected: | |
| Expr _value; | |
| explicit Mod_else(Expr &&fmt) : _value(std::move(fmt)) {} | |
| }; | |
| bool | |
| Mod_else::is_valid_for(ActiveType const &ex_type) const | |
| { | |
| return ex_type.can_satisfy(MaskFor(NIL, STRING)); | |
| } | |
| ActiveType | |
| Mod_else::result_type(ActiveType const &) const | |
| { | |
| return _value.result_type(); | |
| } | |
| Rv<Feature> | |
| Mod_else::operator()(Context &ctx, Feature &feature) | |
| { | |
| return is_empty(feature) ? ctx.extract(_value) : feature; | |
| } | |
| Rv<Modifier::Handle> | |
| Mod_else::load(Config &cfg, YAML::Node, TextView, TextView, YAML::Node key_value) | |
| { | |
| auto &&[fmt, errata]{cfg.parse_expr(key_value)}; | |
| if (!errata.is_ok()) { | |
| errata.note(R"(While parsing "{}" modifier at {}.)", KEY, key_value.Mark()); | |
| return std::move(errata); | |
| } | |
| return Handle(new self_type{std::move(fmt)}); | |
| }; | |
| // --- | |
| /// Concatenate a tuple into a string. | |
| class Mod_join : public Modifier | |
| { | |
| using self_type = Mod_join; | |
| using super_type = Modifier; | |
| public: | |
| static constexpr TextView KEY{"join"}; ///< Identifier name. | |
| using super_type::operator(); // declare hidden member function | |
| /** Modify the feature. | |
| * | |
| * @param ctx Run time context. | |
| * @param feature Feature to modify [in,out] | |
| * @return Errors, if any. | |
| */ | |
| Rv<Feature> operator()(Context &ctx, Feature &feature) override; | |
| /** Check if @a ftype is a valid type to be modified. | |
| * | |
| * @param ftype Type of feature to modify. | |
| * @return @c true if this modifier can modity that feature type, @c false if not. | |
| */ | |
| bool is_valid_for(ActiveType const &ex_type) const override; | |
| /// Resulting type of feature after modifying. | |
| ActiveType result_type(ActiveType const &) const override; | |
| /** Create an instance from YAML config. | |
| * | |
| * @param cfg Configuration state object. | |
| * @param mod_node Node with modifier. | |
| * @param key_node Node in @a mod_node that identifies the modifier. | |
| * @return A constructed instance or errors. | |
| */ | |
| static Rv<Handle> load(Config &cfg, YAML::Node node, TextView key, TextView arg, YAML::Node key_value); | |
| protected: | |
| Expr _separator; | |
| explicit Mod_join(Expr &&fmt) : _separator(std::move(fmt)) {} | |
| }; | |
| bool | |
| Mod_join::is_valid_for(ActiveType const &ex_type) const | |
| { | |
| return ex_type.can_satisfy(MaskFor(NIL, STRING, TUPLE)); | |
| } | |
| ActiveType | |
| Mod_join::result_type(ActiveType const &) const | |
| { | |
| return STRING; | |
| } | |
| Rv<Feature> | |
| Mod_join::operator()(Context &ctx, Feature &feature) | |
| { | |
| // Get the separator - if that doesn't work, leave it empty. | |
| TextView sep; | |
| auto value = ctx.extract(_separator); | |
| if (auto ptr = std::get_if<IndexFor(STRING)>(&value); ptr != nullptr) { | |
| sep = *ptr; | |
| } | |
| return feature.join(ctx, sep); | |
| } | |
| Rv<Modifier::Handle> | |
| Mod_join::load(Config &cfg, YAML::Node, TextView, TextView, YAML::Node key_value) | |
| { | |
| auto &&[expr, errata]{cfg.parse_expr(key_value)}; | |
| if (!errata.is_ok()) { | |
| errata.note(R"(While parsing "{}" modifier at {}.)", KEY, key_value.Mark()); | |
| return std::move(errata); | |
| } | |
| if (!expr.result_type().can_satisfy(STRING)) { | |
| errata.note(R"("{}" modifier at {} requires a string argument.)", KEY, key_value.Mark()); | |
| return std::move(errata); | |
| } | |
| return Handle(new self_type{std::move(expr)}); | |
| }; | |
| // --- | |
| /// Concatenate a string to the active feature. | |
| class Mod_concat : public Modifier | |
| { | |
| using self_type = Mod_concat; | |
| using super_type = Modifier; | |
| public: | |
| static constexpr TextView KEY{"concat"}; ///< Identifier name. | |
| using super_type::operator(); // declare hidden member function | |
| /** Modify the feature. | |
| * | |
| * @param ctx Run time context. | |
| * @param feature Feature to modify [in,out] | |
| * @return Errors, if any. | |
| */ | |
| Rv<Feature> operator()(Context &ctx, Feature &feature) override; | |
| /** Check if @a ftype is a valid type to be modified. | |
| * | |
| * @param ftype Type of feature to modify. | |
| * @return @c true if this modifier can modity that feature type, @c false if not. | |
| */ | |
| bool is_valid_for(ActiveType const &ex_type) const override; | |
| /// Resulting type of feature after modifying. | |
| ActiveType result_type(ActiveType const &) const override; | |
| /** Create an instance from YAML config. | |
| * | |
| * @param cfg Configuration state object. | |
| * @param mod_node Node with modifier. | |
| * @param key_node Node in @a mod_node that identifies the modifier. | |
| * @return A constructed instance or errors. | |
| */ | |
| static Rv<Handle> load(Config &cfg, YAML::Node node, TextView key, TextView arg, YAML::Node key_value); | |
| protected: | |
| Expr _expr; | |
| explicit Mod_concat(Expr &&expr) : _expr(std::move(expr)) {} | |
| struct Visitor { | |
| Context &_ctx; | |
| /// Active feature to modify. | |
| /// This will always be a @c STRING - if not, the visitor isn't called at all. | |
| Feature &_target; | |
| Visitor(Context &ctx, Feature &f) : _ctx(ctx), _target(f) {} | |
| Rv<Feature> operator()(feature_type_for<STRING> const &s); | |
| Rv<Feature> operator()(feature_type_for<TUPLE> const &t); | |
| /// @return An empty string if it is not a handled type. | |
| template <typename F> | |
| auto | |
| operator()(F const &) -> EnableForFeatureTypes<F, Rv<Feature>> | |
| { | |
| return _target; | |
| } | |
| }; | |
| }; | |
| bool | |
| Mod_concat::is_valid_for(ActiveType const &ex_type) const | |
| { | |
| return ex_type.can_satisfy({NIL, STRING}); | |
| } | |
| ActiveType | |
| Mod_concat::result_type(ActiveType const &) const | |
| { | |
| return STRING; | |
| } | |
| Rv<Feature> | |
| Mod_concat::Visitor::operator()(const feature_type_for<STRING> &s) | |
| { | |
| if (s.empty()) { | |
| return _target; | |
| } | |
| TextView src{std::get<IndexFor(STRING)>(_target)}; | |
| _ctx.transient_require(src.size() + s.size()); | |
| auto view = _ctx.render_transient([&](swoc::BufferWriter &w) { w.write(src).write(s); }); | |
| return {_ctx.commit(view)}; | |
| } | |
| Rv<Feature> | |
| Mod_concat::Visitor::operator()(const feature_type_for<TUPLE> &t) | |
| { | |
| TextView src{std::get<IndexFor(STRING)>(_target)}; | |
| if (t[0].index() != IndexFor(STRING) || t[1].index() != IndexFor(STRING)) { | |
| return _target; | |
| } | |
| TextView text{std::get<IndexFor(STRING)>(t[1])}; | |
| if (text.empty()) { | |
| return _target; | |
| } | |
| TextView sep{std::get<IndexFor(STRING)>(t[0])}; | |
| _ctx.transient_require(src.size() + sep.size() + text.size()); | |
| auto view = _ctx.render_transient([=](swoc::BufferWriter &w) { | |
| w.write(src); | |
| if (src.size() && !src.ends_with(sep)) { | |
| w.write(sep); | |
| } | |
| w.write(text); | |
| }); | |
| return {_ctx.commit(view)}; | |
| } | |
| Rv<Feature> | |
| Mod_concat::operator()(Context &ctx, Feature &feature) | |
| { | |
| switch (feature.index()) { | |
| case IndexFor(STRING): | |
| break; | |
| case IndexFor(NIL): // treat NIL as the empty string. | |
| feature = FeatureView::Literal(""); | |
| break; | |
| default: | |
| return feature; | |
| } | |
| Feature f{ctx.extract(_expr)}; | |
| return std::visit(Visitor(ctx, feature), f); | |
| } | |
| Rv<Modifier::Handle> | |
| Mod_concat::load(Config &cfg, YAML::Node, TextView, TextView, YAML::Node key_value) | |
| { | |
| auto &&[expr, errata]{cfg.parse_expr(key_value)}; | |
| if (!errata.is_ok()) { | |
| errata.note(R"(While parsing "{}" modifier at {}.)", KEY, key_value.Mark()); | |
| return std::move(errata); | |
| } | |
| if (!expr.result_type().can_satisfy(MaskFor(STRING, TUPLE))) { | |
| errata.note(R"("{}" modifier at {} requires a string or a list of two strings.)", KEY, key_value.Mark()); | |
| return std::move(errata); | |
| } | |
| return Handle(new self_type{std::move(expr)}); | |
| } | |
| // --- | |
| /// Convert the feature to boolean. | |
| class Mod_as_bool : public Modifier | |
| { | |
| using self_type = Mod_as_bool; | |
| using super_type = Modifier; | |
| public: | |
| inline static const std::string KEY = "as-bool"; ///< Identifier name. | |
| using super_type::operator(); // declare hidden member function | |
| /** Modify the feature. | |
| * | |
| * @param ctx Run time context. | |
| * @param feature Feature to modify [in,out] | |
| * @return Errors, if any. | |
| */ | |
| Rv<Feature> operator()(Context &ctx, Feature &feature) override; | |
| /** Check if @a ftype is a valid type to be modified. | |
| * | |
| * @param ftype Type of feature to modify. | |
| * @return @c true if this modifier can modity that feature type, @c false if not. | |
| */ | |
| bool is_valid_for(ActiveType const &ex_type) const override; | |
| /// Resulting type of feature after modifying. | |
| ActiveType result_type(ActiveType const &) const override; | |
| /** Create an instance from YAML config. | |
| * | |
| * @param cfg Configuration state object. | |
| * @param mod_node Node with modifier. | |
| * @param key_node Node in @a mod_node that identifies the modifier. | |
| * @return A constructed instance or errors. | |
| */ | |
| static Rv<Handle> load(Config &cfg, YAML::Node node, TextView key, TextView arg, YAML::Node key_value); | |
| protected: | |
| inline static const auto VALUE_TYPES = MaskFor({STRING, INTEGER, FLOAT, BOOLEAN, TUPLE, IP_ADDR, NIL}); | |
| Expr _value; ///< Default value. | |
| explicit Mod_as_bool(Expr &&expr) : _value(std::move(expr)) {} | |
| }; | |
| bool | |
| Mod_as_bool::is_valid_for(ActiveType const &ex_type) const | |
| { | |
| return ex_type.can_satisfy(VALUE_TYPES); | |
| } | |
| ActiveType | |
| Mod_as_bool::result_type(ActiveType const &) const | |
| { | |
| return {BOOLEAN}; | |
| } | |
| Rv<Feature> | |
| Mod_as_bool::operator()(Context &, Feature &feature) | |
| { | |
| return {feature.as_bool()}; | |
| } | |
| Rv<Modifier::Handle> | |
| Mod_as_bool::load(Config &cfg, YAML::Node, TextView, TextView, YAML::Node key_value) | |
| { | |
| auto &&[expr, errata]{cfg.parse_expr(key_value)}; | |
| if (!errata.is_ok()) { | |
| errata.note(R"(While parsing "{}" modifier at {}.)", KEY, key_value.Mark()); | |
| return std::move(errata); | |
| } | |
| if (!(expr.is_null() || expr.result_type().can_satisfy(VALUE_TYPES))) { | |
| return Errata(S_ERROR, "Value of {} modifier is not of type {}.", KEY, VALUE_TYPES); | |
| } | |
| return Handle(new self_type{std::move(expr)}); | |
| } | |
| // --- // | |
| /// Convert the feature to an Integer. | |
| class Mod_as_integer : public Modifier | |
| { | |
| using self_type = Mod_as_integer; | |
| using super_type = Modifier; | |
| public: | |
| static constexpr TextView KEY{"as-integer"}; ///< Identifier name. | |
| using super_type::operator(); // declare hidden member function | |
| /** Modify the feature. | |
| * | |
| * @param ctx Run time context. | |
| * @param feature Feature to modify [in,out] | |
| * @return Errors, if any. | |
| */ | |
| Rv<Feature> operator()(Context &ctx, Feature &feature) override; | |
| /** Check if @a ftype is a valid type to be modified. | |
| * | |
| * @param ftype Type of feature to modify. | |
| * @return @c true if this modifier can modity that feature type, @c false if not. | |
| */ | |
| bool is_valid_for(ActiveType const &ex_type) const override; | |
| /// Resulting type of feature after modifying. | |
| ActiveType result_type(ActiveType const &) const override; | |
| /** Create an instance from YAML config. | |
| * | |
| * @param cfg Configuration state object. | |
| * @param mod_node Node with modifier. | |
| * @param key_node Node in @a mod_node that identifies the modifier. | |
| * @return A constructed instance or errors. | |
| */ | |
| static Rv<Handle> load(Config &cfg, YAML::Node node, TextView key, TextView arg, YAML::Node key_value); | |
| protected: | |
| Expr _value; ///< Default value. | |
| explicit Mod_as_integer(Expr &&expr) : _value(std::move(expr)) {} | |
| }; | |
| bool | |
| Mod_as_integer::is_valid_for(ActiveType const &ex_type) const | |
| { | |
| return ex_type.can_satisfy(MaskFor({STRING, INTEGER, FLOAT, BOOLEAN, NIL})); | |
| } | |
| ActiveType | |
| Mod_as_integer::result_type(ActiveType const &) const | |
| { | |
| return {MaskFor({NIL, INTEGER})}; | |
| } | |
| Rv<Feature> | |
| Mod_as_integer::operator()(Context &ctx, Feature &feature) | |
| { | |
| auto &&[value, errata]{feature.as_integer()}; | |
| if (errata.is_ok()) { | |
| return Feature{value}; | |
| } | |
| if (errata.is_ok()) { | |
| return Feature{ctx.extract(_value)}; | |
| } | |
| return feature; | |
| } | |
| Rv<Modifier::Handle> | |
| Mod_as_integer::load(Config &cfg, YAML::Node, TextView, TextView, YAML::Node key_value) | |
| { | |
| auto &&[expr, errata]{cfg.parse_expr(key_value)}; | |
| if (!errata.is_ok()) { | |
| errata.note(R"(While parsing "{}" modifier at {}.)", KEY, key_value.Mark()); | |
| return std::move(errata); | |
| } | |
| if (!(expr.is_null() || expr.result_type().can_satisfy(MaskFor(INTEGER)))) { | |
| return Errata(S_ERROR, "Value of {} modifier is not of type {}.", KEY, INTEGER); | |
| } | |
| return Handle(new self_type{std::move(expr)}); | |
| } | |
| // --- // | |
| /// Convert the feature to an IP address. | |
| class Mod_as_ip_addr : public Modifier | |
| { | |
| using self_type = Mod_as_ip_addr; | |
| using super_type = Modifier; | |
| public: | |
| static const std::string KEY; ///< Identifier name. | |
| using super_type::operator(); // declare hidden member function | |
| /** Modify the feature. | |
| * | |
| * @param ctx Run time context. | |
| * @param feature Feature to modify [in,out] | |
| * @return Errors, if any. | |
| */ | |
| Rv<Feature> operator()(Context &ctx, Feature &feature) override; | |
| /** Check if @a ftype is a valid type to be modified. | |
| * | |
| * @param ftype Type of feature to modify. | |
| * @return @c true if this modifier can modity that feature type, @c false if not. | |
| */ | |
| bool is_valid_for(ActiveType const &ex_type) const override; | |
| /// Resulting type of feature after modifying. | |
| ActiveType result_type(ActiveType const &) const override; | |
| /** Create an instance from YAML config. | |
| * | |
| * @param cfg Configuration state object. | |
| * @param mod_node Node with modifier. | |
| * @param key_node Node in @a mod_node that identifies the modifier. | |
| * @return A constructed instance or errors. | |
| */ | |
| static Rv<Handle> load(Config &cfg, YAML::Node node, TextView key, TextView arg, YAML::Node key_value); | |
| protected: | |
| explicit Mod_as_ip_addr() = default; | |
| /// Identity conversion. | |
| Feature convert(Context &ctx, feature_type_for<IP_ADDR> n); | |
| /// Convert from string | |
| Feature convert(Context &ctx, feature_type_for<STRING> s); | |
| /// Generic failure case. | |
| template <typename T> | |
| auto | |
| convert(Context &, T &) -> EnableForFeatureTypes<T, Feature> | |
| { | |
| return NIL_FEATURE; | |
| } | |
| }; | |
| const std::string Mod_as_ip_addr::KEY{"as-ip-addr"}; | |
| bool | |
| Mod_as_ip_addr::is_valid_for(ActiveType const &ex_type) const | |
| { | |
| return ex_type.can_satisfy(MaskFor({IP_ADDR, STRING})); | |
| } | |
| ActiveType | |
| Mod_as_ip_addr::result_type(ActiveType const &) const | |
| { | |
| return {MaskFor({NIL, IP_ADDR})}; | |
| } | |
| Rv<Feature> | |
| Mod_as_ip_addr::operator()(Context &ctx, Feature &feature) | |
| { | |
| auto visitor = [&](auto &t) { return this->convert(ctx, t); }; | |
| return std::visit(visitor, feature); | |
| } | |
| auto | |
| Mod_as_ip_addr::load(Config &, YAML::Node, TextView, TextView, YAML::Node) -> Rv<Handle> | |
| { | |
| return Handle(new self_type); | |
| } | |
| Feature | |
| Mod_as_ip_addr::convert(Context &, feature_type_for<IP_ADDR> n) | |
| { | |
| return n; | |
| } | |
| Feature | |
| Mod_as_ip_addr::convert(Context &, feature_type_for<STRING> s) | |
| { | |
| swoc::IPAddr addr{s}; | |
| return addr.is_valid() ? Feature{addr} : NIL_FEATURE; | |
| }; | |
| // --- | |
| /// Convert the feature to a Duration. | |
| class Mod_As_Duration : public Modifier | |
| { | |
| using self_type = Mod_As_Duration; | |
| using super_type = Modifier; | |
| public: | |
| static const std::string KEY; ///< Identifier name. | |
| using super_type::operator(); // declare hidden member function | |
| /** Modify the feature. | |
| * | |
| * @param ctx Run time context. | |
| * @param feature Feature to modify [in,out] | |
| * @return Errors, if any. | |
| */ | |
| Rv<Feature> operator()(Context &ctx, Feature &feature) override; | |
| /** Check if @a ftype is a valid type to be modified. | |
| * | |
| * @param ftype Type of feature to modify. | |
| * @return @c true if this modifier can modity that feature type, @c false if not. | |
| */ | |
| bool is_valid_for(ActiveType const &ex_type) const override; | |
| /// Resulting type of feature after modifying. | |
| ActiveType result_type(ActiveType const &) const override; | |
| /** Create an instance from YAML config. | |
| * | |
| * @param cfg Configuration state object. | |
| * @param mod_node Node with modifier. | |
| * @param key_node Node in @a mod_node that identifies the modifier. | |
| * @return A constructed instance or errors. | |
| */ | |
| static Rv<Handle> load(Config &cfg, YAML::Node node, TextView key, TextView arg, YAML::Node key_value); | |
| protected: | |
| Expr _value; ///< Default value. | |
| explicit Mod_As_Duration(Expr &&expr) : _value(std::move(expr)) {} | |
| }; | |
| const std::string Mod_As_Duration::KEY{"as-duration"}; | |
| bool | |
| Mod_As_Duration::is_valid_for(ActiveType const &ex_type) const | |
| { | |
| return ex_type.can_satisfy(MaskFor({STRING, DURATION, TUPLE, NIL})); | |
| } | |
| ActiveType | |
| Mod_As_Duration::result_type(ActiveType const &) const | |
| { | |
| return {MaskFor({NIL, DURATION})}; | |
| } | |
| Rv<Feature> | |
| Mod_As_Duration::operator()(Context &, Feature &feature) | |
| { | |
| auto &&[duration, errata]{feature.as_duration()}; | |
| return {Feature(duration), std::move(errata)}; | |
| } | |
| Rv<Modifier::Handle> | |
| Mod_As_Duration::load(Config &cfg, YAML::Node, TextView, TextView, YAML::Node key_value) | |
| { | |
| auto &&[expr, errata]{cfg.parse_expr(key_value)}; | |
| if (!errata.is_ok()) { | |
| errata.note(R"(While parsing "{}" modifier at {}.)", KEY, key_value.Mark()); | |
| return std::move(errata); | |
| } | |
| return Handle(new self_type{std::move(expr)}); | |
| } | |
| // --- | |
| /// url-encode modifier. | |
| class Mod_url_encode : public Modifier | |
| { | |
| using self_type = Mod_url_encode; | |
| using super_type = Modifier; | |
| // Our own map. TSStringPercentEncode will not escape all we need. | |
| static constexpr unsigned char escape_codes[32] = { | |
| 0xFF, 0xFF, 0xFF, | |
| 0xFF, // control | |
| 0xBE, // space ” # % $ & | |
| 0x19, // + , / | |
| 0x00, // | |
| 0x3F, // < > : ; = ? | |
| 0x80, // @ | |
| 0x00, 0x00, // | |
| 0x1E, 0x80, // [ \ ] ^ ` | |
| 0x00, 0x00, // | |
| 0x1F, // { | } ~ DEL | |
| 0x00, 0x00, 0x00, | |
| 0x00, // all non-ascii characters unmodified | |
| 0x00, 0x00, 0x00, | |
| 0x00, // | |
| 0x00, 0x00, 0x00, | |
| 0x00, // | |
| 0x00, 0x00, 0x00, | |
| 0x00 // | |
| }; | |
| public: | |
| inline static const std::string KEY = "url-encode"; | |
| using super_type::operator(); // declare hidden member function | |
| /** Check if @a ftype is a valid type to be modified. | |
| * | |
| * @param ftype Type of feature to modify. | |
| * @return @c true if this modifier can modity that feature type, @c false if not. | |
| */ | |
| bool is_valid_for(ActiveType const &ex_type) const override; | |
| /// Resulting type of feature after modifying. | |
| ActiveType result_type(ActiveType const &) const override; | |
| /** Modify the feature. | |
| * | |
| * @param ctx Run time context. | |
| * @param feature Feature to modify | |
| * @return Errors, if any. | |
| */ | |
| Rv<Feature> operator()(Context &ctx, feature_type_for<STRING> feature) override; | |
| /** Create an instance from YAML config. | |
| * | |
| * @param cfg Configuration state object. | |
| * @param mod_node Node with modifier. | |
| * @param key_node Node in @a mod_node that identifies the modifier. | |
| * @return A constructed instance or errors. | |
| */ | |
| static Rv<super_type::Handle> load(Config &, YAML::Node, TextView, TextView, YAML::Node); | |
| }; | |
| bool | |
| Mod_url_encode::is_valid_for(ActiveType const &ex_type) const | |
| { | |
| return ex_type.can_satisfy(MaskFor(NIL, STRING)); | |
| } | |
| ActiveType | |
| Mod_url_encode::result_type(ActiveType const &) const | |
| { | |
| return {MaskFor({NIL, STRING})}; | |
| } | |
| Rv<Modifier::Handle> | |
| Mod_url_encode::load(Config &, YAML::Node, TextView, TextView, YAML::Node) | |
| { | |
| return Modifier::Handle(new self_type); | |
| } | |
| Rv<Feature> | |
| Mod_url_encode::operator()(Context &ctx, feature_type_for<STRING> feature) | |
| { | |
| const size_t size = feature.size() * 3; // *3 should suffice. | |
| size_t length; | |
| auto buff = ctx.transient_buffer(size); | |
| if (TS_SUCCESS == TSStringPercentEncode(feature.data(), feature.size(), buff.data(), size, &length, escape_codes)) { | |
| ctx.transient_finalize(length).commit_transient(); // adjust the transient buffer length and commit it. | |
| return {FeatureView::Literal({buff.data(), length})}; // literal because it's committed. | |
| } | |
| return NIL_FEATURE; | |
| } | |
| // --- | |
| /// url-decode modifier | |
| class Mod_url_decode : public Modifier | |
| { | |
| using self_type = Mod_url_decode; | |
| using super_type = Modifier; | |
| public: | |
| inline static const std::string KEY = "url-decode"; | |
| using super_type::operator(); // declare hidden member function | |
| /** Check if @a ftype is a valid type to be modified. | |
| * | |
| * @param ftype Type of feature to modify. | |
| * @return @c true if this modifier can modity that feature type, @c false if not. | |
| */ | |
| bool is_valid_for(ActiveType const &ex_type) const override; | |
| /// Resulting type of feature after modifying. | |
| ActiveType result_type(ActiveType const &) const override; | |
| /** Modify the feature. | |
| * | |
| * @param ctx Run time context. | |
| * @param feature Feature to modify | |
| * @return Errors, if any. | |
| * | |
| */ | |
| Rv<Feature> operator()(Context &ctx, feature_type_for<STRING> feature) override; | |
| /** Create an instance from YAML config. | |
| * | |
| * @param cfg Configuration state object. | |
| * @param mod_node Node with modifier. | |
| * @param key_node Node in @a mod_node that identifies the modifier. | |
| * @return A constructed instance or errors. | |
| */ | |
| static Rv<super_type::Handle> load(Config &, YAML::Node, TextView, TextView, YAML::Node); | |
| }; | |
| bool | |
| Mod_url_decode::is_valid_for(ActiveType const &ex_type) const | |
| { | |
| return ex_type.can_satisfy(MaskFor(NIL, STRING)); | |
| } | |
| ActiveType | |
| Mod_url_decode::result_type(ActiveType const &) const | |
| { | |
| return {MaskFor({NIL, STRING})}; | |
| } | |
| Rv<Modifier::Handle> | |
| Mod_url_decode::load(Config &, YAML::Node, TextView, TextView, YAML::Node) | |
| { | |
| return Modifier::Handle(new self_type); | |
| } | |
| Rv<Feature> | |
| Mod_url_decode::operator()(Context &ctx, feature_type_for<STRING> feature) | |
| { | |
| const size_t size = feature.size(); | |
| size_t length; | |
| auto buff = ctx.transient_buffer(size); | |
| if (TS_SUCCESS == TSStringPercentDecode(feature.data(), size, buff.data(), size, &length)) { | |
| ctx.transient_finalize(length).commit_transient(); // adjust the transient buffer length and commit it. | |
| return {FeatureView::Literal({buff.data(), length})}; // literal because it's committed. | |
| } | |
| return NIL_FEATURE; | |
| } | |
| // --- // | |
| namespace | |
| { | |
| [[maybe_unused]] bool INITIALIZED = []() -> bool { | |
| Modifier::define(Mod_hash::KEY, &Mod_hash::load); | |
| Modifier::define(Mod_else::KEY, &Mod_else::load); | |
| Modifier::define(Mod_join::KEY, &Mod_join::load); | |
| Modifier::define(Mod_concat::KEY, &Mod_concat::load); | |
| Modifier::define(Mod_as_bool::KEY, &Mod_as_bool::load); | |
| Modifier::define(Mod_as_integer::KEY, &Mod_as_integer::load); | |
| Modifier::define(Mod_As_Duration::KEY, &Mod_As_Duration::load); | |
| Modifier::define(Mod_filter::KEY, &Mod_filter::load); | |
| Modifier::define(Mod_as_ip_addr::KEY, &Mod_as_ip_addr::load); | |
| Modifier::define(Mod_rxp_replace::KEY, &Mod_rxp_replace::load); | |
| Modifier::define(Mod_url_encode::KEY, &Mod_url_encode::load); | |
| Modifier::define(Mod_url_decode::KEY, &Mod_url_decode::load); | |
| return true; | |
| }(); | |
| } // namespace |