morph
0.1.0
Asynchronous bridge between GUI thread and business models
Toggle main menu visibility
Loading...
Searching...
No Matches
action_log_json.hpp
Go to the documentation of this file.
1
// SPDX-License-Identifier: Apache-2.0
2
3
#pragma once
4
#include <glaze/glaze.hpp>
5
#include <string>
6
#include <string_view>
7
8
#include "action_log.hpp"
9
10
/// @file
11
/// @brief The `LogEntry` JSON codec, kept apart from `action_log.hpp`.
12
///
13
/// `action_log.hpp` defines `LogEntry`, `Outcome`, `IActionLog` and the
14
/// process-wide log slot; `core/model.hpp` includes it for `IActionLog` alone.
15
/// Were the codec declared there too, every consumer that reaches a model would
16
/// compile `<glaze/glaze.hpp>` whether or not it ever serialised anything, and
17
/// that is a measured cliff rather than a tidiness point: with glaze included,
18
/// `action_log.hpp` is 252,559 preprocessed lines and 2.67 CPU-s, against
19
/// `core/strand.hpp`'s 127,217 and 1.20.
20
///
21
/// Include this header instead when you need `toJson`/`fromJson`. Inside
22
/// morph, exactly one header does: `file_action_log.hpp`.
23
24
/// @brief Reflects `Outcome` as the strings `"Succeeded"`/`"Failed"` rather than
25
/// its underlying `0`/`1`, so a journal line stays readable without
26
/// cross-referencing the enum. `LogEntry` otherwise needs no `glz::meta` at
27
/// all (see `toJson`'s doc comment) -- this is the one field that does.
28
template
<>
29
struct
glz::meta<morph::journal::Outcome> {
30
using
enum morph::journal::Outcome;
31
static
constexpr
auto
value = glz::enumerate(Succeeded, Failed);
32
};
33
34
namespace
morph::journal {
35
36
namespace
detail {
37
38
/// @brief Converts a Glaze error into a `SerializationError`, or does nothing
39
/// if @p errCode reports success.
40
///
41
/// Shared by `toJson` and `fromJson` on purpose, not just for DRY: this is one
42
/// non-template function, so both call through the exact same compiled branch.
43
/// `fromJson`'s failure path is easy to exercise for real (malformed JSON is
44
/// everyday input); `toJson`'s is not — Glaze's buffer-writer has no reachable
45
/// failure mode for a flat struct of strings/integers like `LogEntry` (its
46
/// only write-relevant error codes are for recursion-depth limits `LogEntry`
47
/// can't hit, and `dump_int_error`, which nothing in Glaze's own source ever
48
/// actually raises). Routing both through here means `toJson`'s error branch
49
/// is the same branch `fromJson`'s test already exercises, rather than a
50
/// second, structurally-unreachable copy of the same three lines.
51
/// @param errCode Result of a `glz::write_json`/`glz::read_json` call.
52
/// @param context Buffer or input passed to `glz::format_error` for the message.
53
inline
void
throwOnGlazeError
(
const
glz::error_ctx& errCode, std::string_view context) {
54
if
(errCode) {
55
throw
SerializationError
{glz::format_error(errCode, context)};
56
}
57
}
58
59
/// @brief Write options that escape ASCII control bytes as `\\uXXXX` sequences.
60
///
61
/// glaze 7.4 leaves control bytes (0x00-0x1F) unescaped by default, which
62
/// breaks a `LogEntry` carrying one in `entityKey`/`payload`/`error`/`principal`/
63
/// `idempotencyKey` two ways: RFC 8259 requires those bytes escaped, so the raw
64
/// byte alone yields JSON `fromJson`'s `glz::read` throws on; worse, once the
65
/// same string also contains an escaped `\` or `"`, glaze's chunked writer path
66
/// silently rewrites the control byte as two 0x00 bytes, corrupting the payload
67
/// before it ever reaches disk. This mirrors `morph::wire::detail::EscapingWriteOpts`
68
/// (`core/wire.hpp`) exactly; duplicated here (rather than shared) so this header
69
/// stays free of a `core/` dependency for `MORPH_CLIENT_ONLY`-style consumers
70
/// that only want the journal. Escaping is lossless, so any such byte still
71
/// round-trips through `fromJson` unchanged.
72
struct
EscapingWriteOpts
: glz::opts {
73
/// @brief Emit control bytes as `\\uXXXX` rather than raw.
74
// NOLINTNEXTLINE(readability-identifier-naming) — glaze's option name, matched by name.
75
bool
escape_control_characters
=
true
;
76
};
77
78
}
// namespace detail
79
80
/// @brief Encodes @p entry as JSON.
81
///
82
/// `LogEntry` is a plain aggregate, so Glaze reflects it without a `glz::meta`
83
/// specialisation of its own — the same automatic reflection
84
/// `BRIDGE_REGISTER_ACTION` relies on for user action structs. (Its `outcome`
85
/// field is `Outcome`, which does have a `glz::meta` — see above — so it reads
86
/// back as `"Succeeded"`/`"Failed"`, not `0`/`1`.) Used by sinks that need an
87
/// opaque string representation (`FileActionLog`).
88
///
89
/// Writes with `detail::EscapingWriteOpts` so a raw ASCII control byte in any
90
/// string field (`entityKey`, `payload`, `error`, `principal`, `idempotencyKey`)
91
/// round-trips through `fromJson` instead of producing invalid or silently
92
/// corrupted JSON — see that struct's doc comment.
93
/// @param entry Entry to encode.
94
/// @return The entry as a single line of JSON.
95
/// @throws SerializationError on encode failure (see `detail::throwOnGlazeError`
96
/// for why this is not realistically reachable for `LogEntry`).
97
inline
std::string
toJson
(
const
LogEntry
& entry) {
98
std::string out;
99
detail::throwOnGlazeError
(glz::write<
detail::EscapingWriteOpts
{}>(entry, out), out);
100
return
out;
101
}
102
103
/// @brief Decodes @p json into a `LogEntry`.
104
///
105
/// Reads leniently: `glz::read<glz::opts{.error_on_unknown_keys = false}>`,
106
/// the same stance `morph::wire::decode` takes (`include/morph/core/wire.hpp`) —
107
/// an unknown/extra JSON key (e.g. an additive field written by a newer morph
108
/// build) is ignored rather than rejected. Same duplicate-key caveat as
109
/// `wire::decode`: last-wins, not a security boundary (glaze offers no reject
110
/// option). Syntactically malformed JSON still throws. After a successful
111
/// decode, also enforces the line-format version rule: `v <= kLogFormatVersion`
112
/// decodes normally, `v` greater than this build's `kLogFormatVersion` throws
113
/// — a build refuses to guess at a line format newer than any it has seen.
114
/// @param json One JSON-encoded entry, as `toJson` wrote it.
115
/// @return The decoded entry.
116
/// @throws SerializationError if @p json is not valid JSON, does not decode
117
/// into a `LogEntry`, or decodes with `v` greater than
118
/// `kLogFormatVersion`.
119
inline
LogEntry
fromJson
(std::string_view json) {
120
LogEntry
entry{};
121
// `null_terminated = false`: `json` is a caller-supplied view (a line read from a
122
// journal file, with no guaranteed trailing '\0') — see the identical rationale on
123
// `morph::wire::decode` (`wire.hpp`), whose fuzz harness found the resulting
124
// heap-buffer-overflow in glaze's `skip_ws`.
125
static
constexpr
glz::opts kLenient{.null_terminated =
false
, .error_on_unknown_keys =
false
};
126
detail::throwOnGlazeError
(glz::read<kLenient>(entry, json), json);
127
if
(entry.
v
> kLogFormatVersion) {
128
throw
SerializationError
{
129
"journal::fromJson: line format v"
+ std::to_string(entry.
v
) +
130
" is newer than this build supports (kLogFormatVersion = "
+ std::to_string(kLogFormatVersion) +
")"
};
131
}
132
return
entry;
133
}
134
135
}
// namespace morph::journal
morph::journal::fromJson
LogEntry fromJson(std::string_view json)
Decodes json into a LogEntry.
Definition
action_log_json.hpp:119
morph::journal::toJson
std::string toJson(const LogEntry &entry)
Encodes entry as JSON.
Definition
action_log_json.hpp:97
morph::journal::detail::throwOnGlazeError
void throwOnGlazeError(const glz::error_ctx &errCode, std::string_view context)
Converts a Glaze error into a SerializationError, or does nothing if errCode reports success.
Definition
action_log_json.hpp:53
morph::journal::LogEntry
One recorded execution of an action against a model instance.
Definition
action_log.hpp:46
morph::journal::LogEntry::v
std::uint32_t v
Line-format version this entry was written at.
Definition
action_log.hpp:120
morph::journal::SerializationError
Thrown by toJson/fromJson when LogEntry (de)serialisation fails.
Definition
action_log.hpp:152
morph::journal::detail::EscapingWriteOpts
Write options that escape ASCII control bytes as \\uXXXX sequences.
Definition
action_log_json.hpp:72
morph::journal::detail::EscapingWriteOpts::escape_control_characters
bool escape_control_characters
Emit control bytes as \\uXXXX rather than raw.
Definition
action_log_json.hpp:75
morph
journal
action_log_json.hpp
Generated by
1.17.0