15#include <system_error>
44namespace detail::route {
49[[nodiscard]]
constexpr bool unreserved(
char byte)
noexcept {
50 return (
byte >=
'A' &&
byte <=
'Z') || (
byte >=
'a' &&
byte <=
'z') || (
byte >=
'0' &&
byte <=
'9') ||
51 byte ==
'-' ||
byte ==
'.' ||
byte ==
'_' ||
byte ==
'~';
57[[nodiscard]]
inline std::string
encode(std::string_view
text) {
58 static constexpr std::string_view kHex =
"0123456789ABCDEF";
60 out.reserve(
text.size());
61 for (
char const byte :
text) {
65 auto const code =
static_cast<unsigned char>(byte);
67 out += kHex.at(code >> 4U);
68 out += kHex.at(code & 0x0FU);
77[[nodiscard]]
constexpr int hexDigit(
char digit)
noexcept {
78 if (digit >=
'0' && digit <=
'9') {
81 if (digit >=
'A' && digit <=
'F') {
82 return digit -
'A' + 10;
84 if (digit >=
'a' && digit <=
'f') {
85 return digit -
'a' + 10;
93[[nodiscard]]
inline std::optional<std::string>
decode(std::string_view
text) {
95 out.reserve(
text.size());
96 for (std::size_t i = 0; i <
text.size(); ++i) {
97 char const byte =
text.at(i);
102 if (i + 2 >=
text.size()) {
107 if (high < 0 || low < 0) {
110 out +=
static_cast<char>((
static_cast<unsigned>(high) << 4U) |
static_cast<unsigned>(low));
119[[nodiscard]]
inline std::optional<std::int64_t>
integer(std::string_view
text) {
120 std::int64_t number = 0;
121 char const*
const last = std::to_address(
text.end());
122 auto const [end, error] = std::from_chars(std::to_address(
text.begin()), last, number);
123 if (error != std::errc{} || end != last ||
text.empty()) {
133[[nodiscard]]
inline std::optional<std::string>
textOf(
Type const& type,
Value const& value) {
136 if (std::int64_t
const*
const number = value.
asInt()) {
137 return std::to_string(*number);
141 if (
bool const*
const flag = value.
asBool()) {
142 return *flag ?
"true" :
"false";
152 return std::to_string(number->value.numerator) +
"/" + std::to_string(number->value.denominator) +
153 "@" + std::to_string(number->value.decimalPlaces.value);
157 if (std::int64_t
const*
const number = value.
asInt()) {
158 return "i:" + std::to_string(*number);
178[[nodiscard]]
inline std::optional<Value>
valueOf(
Type const& type, std::string_view
text) {
182 return Value{*number};
186 if (
text ==
"true" ||
text ==
"false") {
192 return Value{std::move(*decoded)};
196 auto const slash =
text.find(
'/');
197 auto const atSign =
text.find(
'@');
198 if (slash == std::string_view::npos || atSign == std::string_view::npos || atSign < slash) {
202 auto const den =
integer(
text.substr(slash + 1, atSign - slash - 1));
203 auto const places =
integer(
text.substr(atSign + 1));
204 if (!num || !den || !places || *den <= 0 || *places < 0 ||
212 if (
text.starts_with(
"i:")) {
214 return Value{*number};
218 if (
text.starts_with(
"s:")) {
220 return Value{std::move(*decoded)};
248[[nodiscard]]
inline std::expected<std::string, std::string>
routeOf(
249 std::string_view screen, std::vector<std::pair<std::string, Type>>
const& declared,
ValueRecord const& params) {
250 std::string route{screen};
251 char separator =
'?';
252 for (
auto const& [name, type] : declared) {
253 auto const given = params.find(name);
254 if (given == params.end() || given->second.isNull()) {
255 if (!type.optional) {
256 return std::unexpected(
"the required param " + name +
" is missing");
262 return std::unexpected(
"the param " + name +
" has no route form for its value");
277[[nodiscard]]
inline std::expected<std::string, std::string>
routeOf(
Document const& document,
289[[nodiscard]]
inline std::expected<Route, std::string>
parseRoute(
290 std::string_view route, std::function<std::shared_ptr<Document const>(std::string_view)>
const& lookup) {
291 auto const refuse = [route](std::string
const& reason) {
292 return std::unexpected(
"route " + std::string{route} +
": " + reason);
294 auto const question = route.find(
'?');
295 Route parsed{.screen = std::string{route.substr(0, question)}, .params = {}};
296 std::shared_ptr<Document const>
const document = lookup ? lookup(parsed.
screen) :
nullptr;
298 return refuse(
"unknown screen " + parsed.
screen);
300 std::string_view rest = question == std::string_view::npos ? std::string_view{} : route.substr(question + 1);
301 while (!rest.empty()) {
302 auto const amp = rest.find(
'&');
303 std::string_view
const pair = rest.substr(0, amp);
304 rest = amp == std::string_view::npos ? std::string_view{} : rest.substr(amp + 1);
305 auto const equals = pair.find(
'=');
306 if (equals == std::string_view::npos) {
307 return refuse(
"a param without a value");
311 return refuse(
"a param name that does not decode");
313 auto const declared = std::ranges::find(document->params, *name, &std::pair<std::string, Type>::first);
314 if (declared == document->params.end()) {
315 return refuse(
"the screen declares no param " + *name);
317 if (parsed.
params.contains(*name)) {
318 return refuse(
"the param " + *name +
" is given twice");
322 return refuse(
"the param " + *name +
" does not parse as its type");
324 parsed.
params.emplace(std::move(*name), std::move(*value));
326 for (
auto const& [name, type] : document->params) {
327 if (!type.optional && !parsed.
params.contains(name)) {
328 return refuse(
"the required param " + name +
" is missing");
The document's typed value (spec 5 §3): what every expression evaluates to.
Decimal const * asDecimal() const noexcept
The decimal, if held.
bool const * asBool() const noexcept
The bool, if held.
std::int64_t const * asInt() const noexcept
The int, if held.
std::string const * asString() const noexcept
The string, if held.
The UI document (spec 5 §3): its model — declarations, commands, view nodes, components and customiza...
Exact rational arithmetic for morph model/action values.
constexpr std::uint32_t kMaxDecimalPlaces
Largest decimal precision the type supports: 10^18 is the greatest power of ten that fits in std::int...
std::expected< std::string, std::string > routeOf(std::string_view screen, std::vector< std::pair< std::string, Type > > const &declared, ValueRecord const ¶ms)
Writes the route of a screen instance: its id, then each param the screen declares that params gives,...
constexpr bool unreserved(char byte) noexcept
Whether a byte is an RFC 3986 unreserved character, written as is in a route.
std::optional< std::int64_t > integer(std::string_view text)
Reads a whole decimal integer.
std::string encode(std::string_view text)
Percent-encodes every byte that is not an RFC 3986 unreserved character.
std::optional< std::string > textOf(Type const &type, Value const &value)
The canonical text of a param's value.
constexpr int hexDigit(char digit) noexcept
The value of a hexadecimal digit.
std::optional< Value > valueOf(Type const &type, std::string_view text)
Reads a param's text.
std::expected< Route, std::string > parseRoute(std::string_view route, std::function< std::shared_ptr< Document const >(std::string_view)> const &lookup)
Reads a route back against the screen it names.
std::optional< std::string > decode(std::string_view text)
Decodes XX escapes.
Strong type for a decimal-precision count.
Strong type for a Rational denominator.
Strong type for a Rational numerator.
Exact rational number with a runtime decimal-precision tag.
An exact decimal: a math::Rational whose DecimalPlaces are the value's declared places.
A loaded and validated UI document: a screen, or the app document (id app, spec 6 §5).
std::string id
The document id, such as lims.results.
std::vector< std::pair< std::string, Type > > params
The typed params, in declaration order.
A route read back: the screen and its params.
bool operator==(Route const &other) const =default
Equality of screen and params.
std::string screen
The screen id.
ValueRecord params
The params, by name; a null optional is absent.
A document type (spec 5 §3): a kind, whether null is allowed, and a list's element type.
The UI document's typed values (ui::Value), its types (ui::Type), their JSON form,...
@ Record
record: a record whose fields the document does not declare.
@ Key
key: an int or a string.
@ Any
Not known to the document: a query result, a reply, an element's value.
std::map< std::string, Value, std::less<> > ValueRecord
A record value: field name to value, ordered by name.
Node text(Text spec)
Static or bound text.