ores.cpp.oresmd.parser_tests
Table of Contents
The Catch2 tests for the oresmd parser: the design doc's worked examples (static),
then the spec tables – Round-trip and Round-trip (extended) rows become round-trip
tests, Rejection rows become REQUIRE_THROWS_AS tests. The worked-example tests and
the per-class specials (the ir metric-default test, the equity point test, the
correlation/inflation/commodity parse tests) are static; everything table-driven
is generated in the file's exact order, with the equity Rejection rows split around
the static parse_equity_with_point test.
1. Template
{{! GENERATED FILE — tangled from ores.cpp.oresmd.parser_tests.org. Edit the org source. }}
{{{cpp_license}}}
#include "ores.marketdata.core/oresmd/oresmd_exception.hpp"
#include "ores.marketdata.core/oresmd/oresmd_parser.hpp"
#include <catch2/catch_test_macros.hpp>
#include <utility>
#include <vector>
namespace {
using namespace ores::marketdata::domain;
using ores::marketdata::core::canonical_values;
using ores::marketdata::core::oresmd_exception;
using ores::marketdata::core::oresmd_parser;
const std::string tags("[oresmd][parser]");
oresmd_uri uri(std::string_view s) {
return oresmd_uri{std::string(s)};
}
}
/*
* One test per worked example in id:C3E053CA-0D4B-480B-9119-E11530160EC1's
* "Worked examples" section, matching that doc's tables exactly.
*/
TEST_CASE("parse_fx_spot_quote", tags) {
const auto id = oresmd_parser::parse(uri("oresmd://fx/eurusd?type=quote"));
const auto& fx = std::get<fx_market_data_identifier>(id);
REQUIRE(fx.pair == "EURUSD");
REQUIRE(fx.type == instrument_type::quote);
}
TEST_CASE("parse_ir_usd_libor_3m_fixing", tags) {
const auto id = oresmd_parser::parse(
uri("oresmd://ir/usd?index=libor&tenor=3m&role=projection&type=fixing"));
const auto& ir = std::get<ir_market_data_identifier>(id);
REQUIRE(ir.ccy == "USD");
REQUIRE(ir.type == instrument_type::fixing);
REQUIRE(ir.index == index_family::libor);
REQUIRE(ir.tenor == "3m");
REQUIRE(ir.role == curve_role::projection);
}
TEST_CASE("parse_ir_usd_libor_6m_fixing_is_structurally_distinct_from_3m", tags) {
const auto id3 = oresmd_parser::parse(
uri("oresmd://ir/usd?index=libor&tenor=3m&role=projection&type=fixing"));
const auto id6 = oresmd_parser::parse(
uri("oresmd://ir/usd?index=libor&tenor=6m&role=projection&type=fixing"));
REQUIRE(std::get<ir_market_data_identifier>(id3).tenor !=
std::get<ir_market_data_identifier>(id6).tenor);
}
TEST_CASE("parse_ir_usd_sofr_discount_fixing", tags) {
const auto id =
oresmd_parser::parse(uri("oresmd://ir/usd?index=sofr&tenor=1d&role=discount&type=fixing"));
const auto& ir = std::get<ir_market_data_identifier>(id);
REQUIRE(ir.ccy == "USD");
REQUIRE(ir.index == index_family::sofr);
REQUIRE(ir.role == curve_role::discount);
}
TEST_CASE("parse_ir_eur_euribor_6m_fixing", tags) {
const auto id = oresmd_parser::parse(
uri("oresmd://ir/eur?index=euribor&tenor=6m&role=projection&type=fixing"));
const auto& ir = std::get<ir_market_data_identifier>(id);
REQUIRE(ir.ccy == "EUR");
REQUIRE(ir.index == index_family::euribor);
}
TEST_CASE("parse_ir_eur_estr_discount_fixing", tags) {
const auto id =
oresmd_parser::parse(uri("oresmd://ir/eur?index=estr&tenor=1d&role=discount&type=fixing"));
const auto& ir = std::get<ir_market_data_identifier>(id);
REQUIRE(ir.ccy == "EUR");
REQUIRE(ir.index == index_family::estr);
REQUIRE(ir.role == curve_role::discount);
}
TEST_CASE("parse_ir_new_rfr_families_fixing_without_tenor", tags) {
// The 16 RFR/IBOR families the SQL CHECK allows beyond the original 6
// (synthetic_ir_curve_generation_configs_create.sql): all overnight-style, so a fixing
// without a tenor must parse. Previously the enum lacked them, so parse_enum threw
// "Unrecognised index value" and provisioning a party from realistic-2026 seed data failed.
const std::vector<std::pair<std::string, index_family>> new_families{
{"saron", index_family::saron},
{"aonia", index_family::aonia},
{"corra", index_family::corra},
{"honia", index_family::honia},
{"sora", index_family::sora},
{"swestr", index_family::swestr},
{"nowa", index_family::nowa},
{"kofr", index_family::kofr},
{"mibor", index_family::mibor},
{"zaronia", index_family::zaronia},
{"destr", index_family::destr},
{"polonia", index_family::polonia},
{"nzonia", index_family::nzonia},
{"shibor", index_family::shibor},
{"tiie", index_family::tiie},
{"taibor", index_family::taibor}};
for (const auto& [name, expected] : new_families) {
const auto id = oresmd_parser::parse(uri("oresmd://ir/usd?index=" + name + "&type=fixing"));
const auto& ir = std::get<ir_market_data_identifier>(id);
REQUIRE(ir.type == instrument_type::fixing);
REQUIRE(ir.index == expected);
REQUIRE_FALSE(ir.tenor.has_value());
}
}
TEST_CASE("parse_ir_swap_quote", tags) {
const auto id = oresmd_parser::parse(uri(
"oresmd://ir/"
"usd?index=libor&tenor=3m&role=projection&type=quote"e=ir_swap&metric=rate&point=5y"));
const auto& ir = std::get<ir_market_data_identifier>(id);
REQUIRE(ir.type == instrument_type::quote);
REQUIRE(ir.quote_type == ir_quote_type::ir_swap);
REQUIRE(ir.metric == metric::rate);
REQUIRE(ir.point == "5y");
}
TEST_CASE("parse_discount_quote", tags) {
const auto id = oresmd_parser::parse(uri("oresmd://ir/usd?index=libor&tenor=3m&role=projection&"
"type=quote"e=discount&metric=rate&point=6m"));
const auto& ir = std::get<ir_market_data_identifier>(id);
REQUIRE(ir.quote_type == ir_quote_type::discount);
REQUIRE(ir.metric == metric::rate);
REQUIRE(ir.point == "6m");
}
TEST_CASE("parse_swaption_vol_populates_tenor_and_vol_struct", tags) {
const auto id = oresmd_parser::parse(uri("oresmd://ir/eur?type=vol&point=5y,2y,atm"));
const auto& ir = std::get<ir_market_data_identifier>(id);
REQUIRE(ir.ccy == "EUR");
REQUIRE(ir.type == instrument_type::vol);
REQUIRE_FALSE(ir.index.has_value());
REQUIRE(ir.tenor == "2y");
REQUIRE_FALSE(ir.role.has_value());
REQUIRE(ir.vol.has_value());
REQUIRE(ir.vol->expiry == "5Y");
REQUIRE(ir.vol->strike == "ATM");
}
TEST_CASE("parse_equity_quote", tags) {
const auto id = oresmd_parser::parse(uri("oresmd://equity/aapl?ccy=usd&type=quote"));
const auto& eq = std::get<equity_market_data_identifier>(id);
REQUIRE(eq.ticker == "AAPL");
REQUIRE(eq.ccy == "USD");
}
TEST_CASE("parse_credit_cds_quote", tags) {
const auto id = oresmd_parser::parse(
uri("oresmd://credit/itraxx-europe?ccy=eur&type=quote"e=cds&point=sr,5y"));
const auto& cr = std::get<credit_market_data_identifier>(id);
REQUIRE(cr.reference_entity == "ITRAXX-EUROPE");
REQUIRE(cr.ccy == "EUR");
REQUIRE(cr.quote_type == credit_quote_type::cds);
REQUIRE(cr.point == "sr,5y");
}
TEST_CASE("parse_credit_hazard_rate", tags) {
const auto id = oresmd_parser::parse(
uri("oresmd://credit/vod?ccy=eur&type=quote"e=hazard_rate&point=sr,5y"));
const auto& cr = std::get<credit_market_data_identifier>(id);
REQUIRE(cr.quote_type == credit_quote_type::hazard_rate);
REQUIRE(cr.point == "sr,5y");
}
TEST_CASE("parse_commodity_quote", tags) {
const auto id = oresmd_parser::parse(uri("oresmd://commodity/gold?ccy=usd&type=quote"));
const auto& co = std::get<commodity_market_data_identifier>(id);
REQUIRE(co.commodity_code == "GOLD");
REQUIRE(co.ccy == "USD");
}
/*
* Round-trips: to_uri(parse(uri)) reparses to an identical identifier.
*/
{{#fx_round_trip}}
TEST_CASE("{{description}}", tags) {
const auto original = oresmd_parser::parse(uri("{{{uri}}}"));
const auto roundtripped = oresmd_parser::parse(oresmd_parser::to_uri(original));
REQUIRE(original == roundtripped);
}
{{/fx_round_trip}}
{{#ir_round_trip}}
TEST_CASE("{{description}}", tags) {
const auto original = oresmd_parser::parse(uri("{{{uri}}}"));
const auto roundtripped = oresmd_parser::parse(oresmd_parser::to_uri(original));
REQUIRE(original == roundtripped);
}
{{/ir_round_trip}}
{{#equity_round_trip}}
TEST_CASE("{{description}}", tags) {
const auto original = oresmd_parser::parse(uri("{{{uri}}}"));
const auto roundtripped = oresmd_parser::parse(oresmd_parser::to_uri(original));
REQUIRE(original == roundtripped);
}
{{/equity_round_trip}}
{{#credit_round_trip}}
TEST_CASE("{{description}}", tags) {
const auto original = oresmd_parser::parse(uri("{{{uri}}}"));
const auto roundtripped = oresmd_parser::parse(oresmd_parser::to_uri(original));
REQUIRE(original == roundtripped);
}
{{/credit_round_trip}}
{{#commodity_round_trip}}
TEST_CASE("{{description}}", tags) {
const auto original = oresmd_parser::parse(uri("{{{uri}}}"));
const auto roundtripped = oresmd_parser::parse(oresmd_parser::to_uri(original));
REQUIRE(original == roundtripped);
}
{{/commodity_round_trip}}
/*
* Round-trip tests for new IR quote types (id:D566131C-D08C-4AFE-950E-B3DD26EB2C24).
*/
{{#ir_round_trip_extended}}
TEST_CASE("{{description}}", tags) {
const auto original = oresmd_parser::parse(uri("{{{uri}}}"));
const auto roundtripped = oresmd_parser::parse(oresmd_parser::to_uri(original));
REQUIRE(original == roundtripped);
}
{{/ir_round_trip_extended}}
/*
* Invalid cases: a field that belongs to a different asset class must be rejected,
* not silently ignored -- see id:C3E053CA-0D4B-480B-9119-E11530160EC1's
* per-asset-class conditionality.
*/
{{#fx_rejection}}
TEST_CASE("{{description}}", tags) {
REQUIRE_THROWS_AS(oresmd_parser::parse(uri("{{{uri}}}")), oresmd_exception);
}
{{/fx_rejection}}
{{#ir_rejection}}
TEST_CASE("{{description}}", tags) {
REQUIRE_THROWS_AS(oresmd_parser::parse(uri("{{{uri}}}")), oresmd_exception);
}
{{/ir_rejection}}
TEST_CASE("parse_ir_metric_present_when_type_is_omitted_defaults_to_quote", tags) {
const auto id = oresmd_parser::parse(uri("oresmd://ir/usd?index=libor&tenor=3m&metric=rate"));
const auto& ir = std::get<ir_market_data_identifier>(id);
REQUIRE(ir.type == instrument_type::quote);
REQUIRE(ir.metric == metric::rate);
}
TEST_CASE("parse_equity_dividend_quote", tags) {
const auto id =
oresmd_parser::parse(uri("oresmd://equity/aapl?ccy=usd&type=quote"e=dividend"));
const auto& eq = std::get<equity_market_data_identifier>(id);
REQUIRE(eq.quote_type == equity_quote_type::dividend);
}
{{#equity_rejection}}
TEST_CASE("{{description}}", tags) {
REQUIRE_THROWS_AS(oresmd_parser::parse(uri("{{{uri}}}")), oresmd_exception);
}
{{/equity_rejection}}
TEST_CASE("parse_equity_with_point", tags) {
const auto id = oresmd_parser::parse(
uri("oresmd://equity/aapl?ccy=usd&type=quote"e=dividend&point=1y"));
const auto& eq = std::get<equity_market_data_identifier>(id);
REQUIRE(eq.quote_type == equity_quote_type::dividend);
REQUIRE(eq.point == "1y");
}
{{#equity_rejection_tail}}
TEST_CASE("{{description}}", tags) {
REQUIRE_THROWS_AS(oresmd_parser::parse(uri("{{{uri}}}")), oresmd_exception);
}
{{/equity_rejection_tail}}
{{#credit_rejection}}
TEST_CASE("{{description}}", tags) {
REQUIRE_THROWS_AS(oresmd_parser::parse(uri("{{{uri}}}")), oresmd_exception);
}
{{/credit_rejection}}
TEST_CASE("parse_correlation_pairwise", tags) {
const auto id =
oresmd_parser::parse(uri("oresmd://correlation/ccy-eur-usd?type=quote"e=pairwise"));
const auto& cr = std::get<correlation_market_data_identifier>(id);
REQUIRE(cr.factor_pair == "CCY-EUR-USD");
REQUIRE(cr.quote_type == correlation_quote_type::pairwise);
}
{{#correlation_round_trip}}
TEST_CASE("{{description}}", tags) {
const auto original = oresmd_parser::parse(uri("{{{uri}}}"));
const auto roundtripped = oresmd_parser::parse(oresmd_parser::to_uri(original));
REQUIRE(original == roundtripped);
}
{{/correlation_round_trip}}
TEST_CASE("parse_inflation_zc_swap", tags) {
const auto id =
oresmd_parser::parse(uri("oresmd://inflation/ukrpi?type=quote"e=zc_swap&point=5y"));
const auto& inf = std::get<inflation_market_data_identifier>(id);
REQUIRE(inf.index_code == "UKRPI");
REQUIRE(inf.quote_type == inflation_quote_type::zc_swap);
REQUIRE(inf.point == "5y");
}
{{#inflation_round_trip}}
TEST_CASE("{{description}}", tags) {
const auto original = oresmd_parser::parse(uri("{{{uri}}}"));
const auto roundtripped = oresmd_parser::parse(oresmd_parser::to_uri(original));
REQUIRE(original == roundtripped);
}
{{/inflation_round_trip}}
TEST_CASE("parse_commodity_fwd_quote", tags) {
const auto id =
oresmd_parser::parse(uri("oresmd://commodity/gold?ccy=usd&type=quote"e=fwd&point=6m"));
const auto& co = std::get<commodity_market_data_identifier>(id);
REQUIRE(co.quote_type == commodity_quote_type::fwd);
REQUIRE(co.point == "6m");
}
{{#commodity_rejection}}
TEST_CASE("{{description}}", tags) {
REQUIRE_THROWS_AS(oresmd_parser::parse(uri("{{{uri}}}")), oresmd_exception);
}
{{/commodity_rejection}}
TEST_CASE("reject_unrecognised_scheme", tags) {
REQUIRE_THROWS_AS(oresmd_parser::parse(uri("https://fx/eurusd?type=quote")), oresmd_exception);
}
TEST_CASE("reject_unrecognised_asset_class", tags) {
REQUIRE_THROWS_AS(oresmd_parser::parse(uri("oresmd://bogus/eurusd?type=quote")),
oresmd_exception);
}
TEST_CASE("reject_unrecognised_index_family_value", tags) {
REQUIRE_THROWS_AS(oresmd_parser::parse(uri("oresmd://ir/usd?index=bogus&tenor=3m&type=fixing")),
oresmd_exception);
}
TEST_CASE("reject_unrecognised_instrument_type_value", tags) {
REQUIRE_THROWS_AS(oresmd_parser::parse(uri("oresmd://fx/eurusd?type=bogus")), oresmd_exception);
}
TEST_CASE("reject_malformed_uri", tags) {
REQUIRE_THROWS_AS(oresmd_parser::parse(uri("not a uri at all")), oresmd_exception);
}
TEST_CASE("reject_unrecognised_query_key", tags) {
REQUIRE_THROWS_AS(oresmd_parser::parse(uri("oresmd://ir/usd?index=libor&tenr=3m&type=fixing")),
oresmd_exception);
}
TEST_CASE("reject_fx_quote_when_type_not_quote", tags) {
REQUIRE_THROWS_AS(oresmd_parser::parse(uri("oresmd://fx/eurusd?type=fixing"e=spot")),
oresmd_exception);
}
TEST_CASE("reject_fx_entity_that_is_not_a_six_letter_pair", tags) {
REQUIRE_THROWS_AS(oresmd_parser::parse(uri("oresmd://fx/eur?type=quote")), oresmd_exception);
}
TEST_CASE("reject_ir_term_index_fixing_without_a_tenor", tags) {
// A term index (libor/euribor) needs a tenor to disambiguate which point on the
// curve it fixes at; an overnight index (sofr/estr/...) does not have this
// requirement -- see parse_ir_usd_sofr_discount_fixing above, which omits it too
// (its tenor is supplied for the curve key, but the check below is term-index-only).
REQUIRE_THROWS_AS(oresmd_parser::parse(uri("oresmd://ir/usd?index=libor&type=fixing")),
oresmd_exception);
}
/*
* Canonical form contract (acceptance d): to_uri(parse(uri)) == uri for URIs already
* in canonical form — the fixed per-asset-class parameter order, lowercased
* components, absent fields skipped. This pins the canonical string equality lookup
* compares against.
*/
TEST_CASE("canonical_string_round_trip_fx_spot", tags) {
const auto s = uri("oresmd://fx/eurusd?type=quote"e=spot");
REQUIRE(oresmd_parser::to_uri(oresmd_parser::parse(s)).value == s.value);
}
TEST_CASE("canonical_string_round_trip_fx_fwd", tags) {
const auto s = uri("oresmd://fx/eurusd?type=quote"e=fwd&point=6m");
REQUIRE(oresmd_parser::to_uri(oresmd_parser::parse(s)).value == s.value);
}
TEST_CASE("canonical_string_round_trip_ir_fixing", tags) {
const auto s = uri("oresmd://ir/usd?index=libor&tenor=3m&role=projection&type=fixing");
REQUIRE(oresmd_parser::to_uri(oresmd_parser::parse(s)).value == s.value);
}
TEST_CASE("canonical_string_round_trip_ir_quote", tags) {
const auto s = uri(
"oresmd://ir/"
"usd?index=libor&tenor=3m&role=projection&type=quote&metric=rate"e=ir_swap&point=5y");
REQUIRE(oresmd_parser::to_uri(oresmd_parser::parse(s)).value == s.value);
}
TEST_CASE("canonical_string_round_trip_ir_vol", tags) {
const auto s = uri("oresmd://ir/eur?tenor=2y&type=vol&point=5y,2y,atm");
REQUIRE(oresmd_parser::to_uri(oresmd_parser::parse(s)).value == s.value);
}
TEST_CASE("canonical_string_round_trip_equity", tags) {
const auto s = uri("oresmd://equity/aapl?ccy=usd&type=quote"e=dividend&point=1y");
REQUIRE(oresmd_parser::to_uri(oresmd_parser::parse(s)).value == s.value);
}
TEST_CASE("canonical_string_round_trip_credit", tags) {
const auto s =
uri("oresmd://credit/itraxx-europe?ccy=eur&type=quote"e=cds&point=sr,5y");
REQUIRE(oresmd_parser::to_uri(oresmd_parser::parse(s)).value == s.value);
}
TEST_CASE("canonical_string_round_trip_commodity", tags) {
const auto s = uri("oresmd://commodity/wti?ccy=usd&type=quote"e=fwd&point=6m");
REQUIRE(oresmd_parser::to_uri(oresmd_parser::parse(s)).value == s.value);
}
TEST_CASE("canonical_string_round_trip_inflation", tags) {
const auto s = uri("oresmd://inflation/ukrpi?type=quote"e=zc_swap&point=5y");
REQUIRE(oresmd_parser::to_uri(oresmd_parser::parse(s)).value == s.value);
}
TEST_CASE("canonical_string_round_trip_correlation", tags) {
const auto s = uri("oresmd://correlation/ccy-eur-usd?type=quote"e=pairwise");
REQUIRE(oresmd_parser::to_uri(oresmd_parser::parse(s)).value == s.value);
}
TEST_CASE("stored_form_is_the_canonical_encoding_independent_of_input_spelling", tags) {
// The stored string is exactly what to_uri() emits for the parsed identifier, never
// the input's own spelling: the percent-encoded "%2F" and the raw "/" spellings
// both settle on the same stored form, so matching compares like with like. (The
// encoder leaves "/" unencoded in a query value -- both spellings normalise to it.)
const auto encoded_input =
uri("oresmd://ir/eur?type=quote&metric=rate"e=ir_swap&point=5y%2F6m");
const auto raw_input = uri("oresmd://ir/eur?type=quote&metric=rate"e=ir_swap&point=5y/6m");
const auto id = oresmd_parser::parse(encoded_input);
REQUIRE(std::get<ir_market_data_identifier>(id).point == "5y/6m");
REQUIRE(oresmd_parser::parse(raw_input) == id);
const auto out = oresmd_parser::to_uri(id);
REQUIRE(out.value == "oresmd://ir/eur?type=quote&metric=rate"e=ir_swap&point=5y/6m");
REQUIRE(oresmd_parser::parse(out) == id);
REQUIRE(oresmd_parser::to_uri(oresmd_parser::parse(raw_input)).value == out.value);
}
/*
* Canonical values container (acceptance e): to_uri(identifier, canonical) matches the
* identifier's tenor and point against the supplied container and rejects unknown
* spellings — oresmd keeps no dependency on the refdata repositories.
*/
TEST_CASE("to_uri_with_canonical_values_accepts_known_spellings", tags) {
canonical_values cv;
cv.tenor = {"3m", "1d"};
cv.point = {"5y"};
const auto id = oresmd_parser::parse(
uri("oresmd://ir/"
"usd?index=libor&tenor=3m&role=projection&type=quote&metric=rate"e=ir_swap&point=5y"));
REQUIRE(oresmd_parser::to_uri(id, cv).value ==
"oresmd://ir/"
"usd?index=libor&tenor=3m&role=projection&type=quote&metric=rate"e=ir_swap&point=5y");
}
TEST_CASE("to_uri_with_canonical_values_rejects_an_unknown_tenor_spelling", tags) {
canonical_values cv;
cv.tenor = {"6m"}; // the identifier's "3m" is not canonical
cv.point = {"5y"};
const auto id = oresmd_parser::parse(
uri("oresmd://ir/usd?index=libor&tenor=3m&type=quote"e=ir_swap&point=5y"));
REQUIRE_THROWS_AS(oresmd_parser::to_uri(id, cv), oresmd_exception);
}
TEST_CASE("to_uri_with_canonical_values_rejects_an_unknown_point_spelling", tags) {
canonical_values cv;
cv.tenor = {"3m"};
cv.point = {"6m"}; // the identifier's "5y" is not canonical
const auto id = oresmd_parser::parse(
uri("oresmd://ir/usd?index=libor&tenor=3m&type=quote"e=ir_swap&point=5y"));
REQUIRE_THROWS_AS(oresmd_parser::to_uri(id, cv), oresmd_exception);
}
TEST_CASE("to_uri_with_canonical_values_rejects_an_unknown_credit_point_spelling", tags) {
canonical_values cv;
cv.point = {"sr,5y"};
const auto id = oresmd_parser::parse(
uri("oresmd://credit/vod?ccy=eur&type=quote"e=cds&point=sr,10y"));
REQUIRE_THROWS_AS(oresmd_parser::to_uri(id, cv), oresmd_exception);
}
TEST_CASE("to_uri_with_canonical_values_accepts_a_vol_composite_point", tags) {
// A type=vol identifier stores the whole composite "5y,2y,atm" as its point, so
// the container matches the composite, not its parts; the parser also lifts the
// composite's middle part into the identifier's tenor, and the canonical URI
// serializes it (parse normalises the tenor-less input spelling).
canonical_values cv;
cv.tenor = {"2y"};
cv.point = {"5y,2y,atm"};
const auto id = oresmd_parser::parse(uri("oresmd://ir/eur?type=vol&point=5y,2y,atm"));
REQUIRE(oresmd_parser::to_uri(id, cv).value ==
"oresmd://ir/eur?tenor=2y&type=vol&point=5y,2y,atm");
}
TEST_CASE("to_uri_with_canonical_values_rejects_a_vol_point_that_is_not_the_composite", tags) {
canonical_values cv;
cv.tenor = {"2y"};
cv.point = {"5y", "2y", "atm"}; // the identifier's composite "5y,2y,atm" is not canonical
const auto id = oresmd_parser::parse(uri("oresmd://ir/eur?type=vol&point=5y,2y,atm"));
REQUIRE_THROWS_AS(oresmd_parser::to_uri(id, cv), oresmd_exception);
}
TEST_CASE("to_uri_with_empty_canonical_values_passes_scalar_identifiers", tags) {
// Scalars carry no tenor or point, so an empty container is fine for them.
const canonical_values cv;
const auto fx = oresmd_parser::parse(uri("oresmd://fx/eurusd?type=quote"e=spot"));
REQUIRE(oresmd_parser::to_uri(fx, cv).value == "oresmd://fx/eurusd?type=quote"e=spot");
const auto cr = oresmd_parser::parse(uri("oresmd://correlation/ccy-eur-usd?type=quote"e=pairwise"));
REQUIRE(oresmd_parser::to_uri(cr, cv).value ==
"oresmd://correlation/ccy-eur-usd?type=quote"e=pairwise");
}