ores.cpp.oresmd.parser

Table of Contents

The parser for oresmd URIs: one parse_<asset_class> function per class, generated from the spec's Fields table (entity field, ccy requirement, quote/point handling), the Reject keys table (the validate_* functions and the inline rejects), and the Type-gated keys table (ir). to_uri serializes the query keys in the spec's URI order. Asset-class-keyed sections (fx, ir, …) hold the genuinely per-class specials; the two per-class orders come from the manifest's variant_order and parse_order columns.

1. Template

{{! GENERATED FILE — tangled from ores.cpp.oresmd.parser.org. Edit the org source. }}
{{{cpp_license}}}
#include "ores.marketdata.core/oresmd/oresmd_parser.hpp"
#include "ores.marketdata.core/oresmd/detail/oresmd_index_family_utils.hpp"
#include "ores.marketdata.core/oresmd/detail/oresmd_string_utils.hpp"
#include "ores.marketdata.core/oresmd/oresmd_exception.hpp"
#include <boost/throw_exception.hpp>
#include <boost/url.hpp>
#include <algorithm>
#include <cctype>
#include <format>
#include <magic_enum/magic_enum.hpp>
#include <optional>
#include <string>
#include <vector>

namespace {

using namespace ores::marketdata::domain;
using ores::marketdata::core::oresmd_exception;
using ores::marketdata::core::detail::is_overnight;
using ores::marketdata::core::detail::to_lower;
using ores::marketdata::core::detail::to_upper;

/**
 * @brief Query parameters of an oresmd URI, decoded once and indexed by key -- every
 * asset class reads a subset of these, none needs the raw params_view.
 */
struct query_params final {
    std::optional<std::string> ccy;
    std::optional<std::string> index;
    std::optional<std::string> tenor;
    std::optional<std::string> role;
    std::optional<std::string> type;
    std::optional<std::string> metric;
    std::optional<std::string> quote;
    std::optional<std::string> model;
    std::optional<std::string> point;

    static query_params from(const boost::urls::url_view& u) {
        query_params qp;
        for (const auto& p : u.params()) {
            if (p.key == "ccy")
                qp.ccy = p.value;
            else if (p.key == "index")
                qp.index = p.value;
            else if (p.key == "tenor")
                qp.tenor = p.value;
            else if (p.key == "role")
                qp.role = p.value;
            else if (p.key == "type")
                qp.type = p.value;
            else if (p.key == "metric")
                qp.metric = p.value;
            else if (p.key == "quote")
                qp.quote = p.value;
            else if (p.key == "model")
                qp.model = p.value;
            else if (p.key == "point")
                qp.point = p.value;
            else
                BOOST_THROW_EXCEPTION(
                    oresmd_exception(std::format("Unrecognised oresmd query key: '{}'.", p.key)));
        }
        return qp;
    }
};

template <typename Enum>
Enum parse_enum(std::string_view field, std::string_view value) {
    const auto e = magic_enum::enum_cast<Enum>(value);
    if (!e)
        BOOST_THROW_EXCEPTION(
            oresmd_exception(std::format("Unrecognised {} value: {}", field, value)));
    return *e;
}

instrument_type parse_type(const query_params& qp) {
    if (!qp.type)
        return instrument_type::quote;
    return parse_enum<instrument_type>("type", *qp.type);
}

/**
 * @brief Throws if @p value is present -- used to reject a query key another asset class
 * uses but this one does not (e.g. =tenor= on an FX URI), rather than silently ignoring
 * it. Every asset class's allowed key set is documented in the design doc's "Grammar"
 * section; anything outside it for a given =asset_class= is a genuine input error, not
 * forward-compatible noise.
 */
void reject_if_present(std::string_view asset_class,
                       std::string_view key,
                       const std::optional<std::string>& value) {
    if (value)
        BOOST_THROW_EXCEPTION(oresmd_exception(
            std::format("oresmd://{}/... does not support the '{}' query key.", asset_class, key)));
}

{{#oresmd_validate_pre}}
void validate_{{asset_class}}(const query_params& qp) {
{{#reject_keys}}
    reject_if_present("{{asset_class}}", "{{.}}", qp.{{.}});
{{/reject_keys}}
{{#type_gated_keys}}
    if (qp.{{.}} && parse_type(qp) != instrument_type::quote)
        BOOST_THROW_EXCEPTION(
            oresmd_exception("oresmd://{{asset_class}}/... '{{.}}' is only meaningful when type=quote."));
{{/type_gated_keys}}
}

{{/oresmd_validate_pre}}
void validate_no_ir_only_keys(std::string_view asset_class, const query_params& qp) {
    reject_if_present(asset_class, "index", qp.index);
    reject_if_present(asset_class, "tenor", qp.tenor);
    reject_if_present(asset_class, "role", qp.role);
    reject_if_present(asset_class, "metric", qp.metric);
}

{{#oresmd_validate_post}}
void validate_{{asset_class}}(const query_params& qp) {
    validate_no_ir_only_keys("{{asset_class}}", qp);
}

{{/oresmd_validate_post}}
std::string first_segment(const boost::urls::url_view& u) {
    for (const auto seg : u.segments()) {
        if (!seg.empty())
            return std::string(seg);
    }
    BOOST_THROW_EXCEPTION(oresmd_exception("oresmd URI is missing its entity path segment."));
}

{{#oresmd_parse_specs}}
{{#ir}}
market_data_identifier parse_ir(const boost::urls::url_view& u, const query_params& qp) {
    validate_ir(qp);
    ir_market_data_identifier id;
    id.ccy = to_upper(first_segment(u));
    id.type = parse_type(qp);
    if (qp.index)
        id.index = parse_enum<index_family>("index", *qp.index);
    if (qp.tenor)
        id.tenor = to_lower(*qp.tenor);
    if (qp.role)
        id.role = parse_enum<curve_role>("role", *qp.role);
    if (qp.metric)
        id.metric = parse_enum<metric>("metric", *qp.metric);
    if (qp.quote)
        id.quote_type = parse_enum<ir_quote_type>("quote", *qp.quote);
    if (qp.point) {
        id.point = to_lower(*qp.point);
        if (id.type == instrument_type::vol) {
            std::vector<std::string> parts;
            std::stringstream ss(*id.point);
            std::string part;
            while (std::getline(ss, part, ','))
                parts.push_back(to_upper(part));
            if (parts.size() == 3) {
                volatility_surface_point v;
                v.expiry = parts[0];
                if (!id.tenor)
                    id.tenor = to_lower(parts[1]);
                v.strike = parts[2];
                id.vol = std::move(v);
            }
        }
    }
    if (qp.model && id.type == instrument_type::vol && id.vol)
        id.vol->model_subtype = parse_enum<volatility_model_subtype>("model", *qp.model);
    if (id.type == instrument_type::fixing && id.index && !is_overnight(*id.index) && !id.tenor)
        BOOST_THROW_EXCEPTION(oresmd_exception(
            std::format("oresmd://ir/... a term index ('{}') fixing requires a tenor.",
                        magic_enum::enum_name(*id.index))));
    return id;
}

{{/ir}}
{{^ir}}
market_data_identifier parse_{{asset_class}}(const boost::urls::url_view& u, const query_params& qp) {
{{#validate_function_call}}
    validate_{{asset_class}}(qp);
{{/validate_function_call}}
{{#validate_inline_delegate}}
    validate_no_ir_only_keys("{{asset_class}}", qp);
{{/validate_inline_delegate}}
{{#validate_inline}}
{{#inflation}}
    // Validation: only quote, type, and point are meaningful for inflation.
{{/inflation}}
{{#reject_keys}}
    reject_if_present("{{asset_class}}", "{{.}}", qp.{{.}});
{{/reject_keys}}
{{/validate_inline}}
    {{asset_class}}_market_data_identifier id;
    id.{{entity_field}} = to_upper(first_segment(u));
{{#fx}}
    if (id.pair.size() != 6 ||
        !std::ranges::all_of(id.pair, [](unsigned char c) { return std::isalpha(c); }))
        BOOST_THROW_EXCEPTION(oresmd_exception(std::format(
            "oresmd://fx/... entity must be a 6-letter currency pair, got: '{}'.", id.pair)));
{{/fx}}
{{#requires_ccy}}
    if (!qp.ccy)
        BOOST_THROW_EXCEPTION(oresmd_exception("oresmd://{{asset_class}}/... requires a ccy query key."));
    id.ccy = to_upper(*qp.ccy);
{{/requires_ccy}}
    id.type = parse_type(qp);
{{#has_quote_type}}
{{#quote_type_checked}}
    if (qp.quote) {
        if (id.type != instrument_type::quote)
            BOOST_THROW_EXCEPTION(oresmd_exception(
                "oresmd://{{asset_class}}/... 'quote' is only meaningful when type=quote."));
        id.quote_type = parse_enum<{{asset_class}}_quote_type>("quote", *qp.quote);
    }
{{/quote_type_checked}}
{{^quote_type_checked}}
    if (qp.quote)
        id.quote_type = parse_enum<{{asset_class}}_quote_type>("quote", *qp.quote);
{{/quote_type_checked}}
{{/has_quote_type}}
{{#has_point}}
    if (qp.point)
        id.point = to_lower(*qp.point);
{{/has_point}}
    return id;
}

{{/ir}}
{{/oresmd_parse_specs}}
void append_if(boost::urls::url& u, std::string_view key, const std::optional<std::string>& v) {
    if (v)
        u.params().append({key, *v});
}

template <typename Enum>
void append_enum_if(boost::urls::url& u, std::string_view key, const std::optional<Enum>& v) {
    if (v)
        u.params().append({key, std::string(magic_enum::enum_name(*v))});
}

}

namespace ores::marketdata::core {

namespace {

// Which identifier fields the canonical container governs, per asset class: the
// ir identifier carries tenor and point; fx, equity, credit, commodity, and
// inflation carry point; correlation carries neither.
void validate_canonical(const domain::market_data_identifier& identifier,
                        const canonical_values& canonical) {
    const auto check_tenor = [&canonical](const std::optional<std::string>& v) {
        if (v && !canonical.tenor.contains(*v))
            BOOST_THROW_EXCEPTION(oresmd_exception(std::format(
                "Unknown tenor spelling '{}': not in the supplied canonical values.", *v)));
    };
    const auto check_point = [&canonical](const std::optional<std::string>& v) {
        if (v && !canonical.point.contains(*v))
            BOOST_THROW_EXCEPTION(oresmd_exception(std::format(
                "Unknown point spelling '{}': not in the supplied canonical values.", *v)));
    };

    std::visit(
        [&](const auto& id) {
            using T = std::decay_t<decltype(id)>;
            if constexpr (std::is_same_v<T, ir_market_data_identifier>) {
                check_tenor(id.tenor);
                check_point(id.point);
            } else if constexpr (std::is_same_v<T, fx_market_data_identifier> ||
                                 std::is_same_v<T, equity_market_data_identifier> ||
                                 std::is_same_v<T, credit_market_data_identifier> ||
                                 std::is_same_v<T, commodity_market_data_identifier> ||
                                 std::is_same_v<T, inflation_market_data_identifier>) {
                check_point(id.point);
            }
        },
        identifier);
}

}

market_data_identifier oresmd_parser::parse(const domain::oresmd_uri& uri) {
    const auto r = boost::urls::parse_uri(uri.value);
    if (r.has_error())
        BOOST_THROW_EXCEPTION(
            oresmd_exception(std::format("Failed to parse oresmd URI: {}", uri.value)));

    const auto& u = r.value();
    if (u.scheme() != domain::oresmd_scheme)
        BOOST_THROW_EXCEPTION(oresmd_exception(std::format(
            "Unrecognised URI scheme: {} (expected {})", u.scheme(), domain::oresmd_scheme)));

    const auto asset_token = to_lower(u.host());
    const auto qp = query_params::from(u);

{{#oresmd_variant_specs}}
    if (asset_token == "{{asset_class}}")
        return parse_{{asset_class}}(u, qp);
{{/oresmd_variant_specs}}

    BOOST_THROW_EXCEPTION(
        oresmd_exception(std::format("Unrecognised oresmd asset class: {}", asset_token)));
}

domain::oresmd_uri oresmd_parser::to_uri(const domain::market_data_identifier& identifier) {
    boost::urls::url u;
    u.set_scheme(domain::oresmd_scheme);

    std::visit(
        [&u](const auto& id) {
            using T = std::decay_t<decltype(id)>;
{{#oresmd_variant_specs}}
{{#variant_first}}            if constexpr{{/variant_first}}{{^variant_first}}            } else if constexpr{{/variant_first}} (std::is_same_v<T, {{asset_class}}_market_data_identifier>) {
                u.set_host("{{asset_class}}");
                u.segments().push_back(to_lower(id.{{entity_field}}));
{{#uri_order}}
{{#is_type}}
                u.params().append({"type", std::string(magic_enum::enum_name(id.type))});
{{/is_type}}
{{#is_ccy}}
                u.params().append({"ccy", to_lower(id.ccy)});
{{/is_ccy}}
{{#is_enum}}
                append_enum_if(u, "{{key}}", id.{{name}});
{{/is_enum}}
{{#is_string}}
                append_if(u, "{{key}}", id.{{name}});
{{/is_string}}
{{/uri_order}}
{{/oresmd_variant_specs}}
            }
        },
        identifier);

    return domain::oresmd_uri{u.buffer()};
}

domain::oresmd_uri oresmd_parser::to_uri(const domain::market_data_identifier& identifier,
                                         const canonical_values& canonical) {
    validate_canonical(identifier, canonical);
    return to_uri(identifier);
}

}

Emacs 29.3 (Org mode 9.6.15)