JSON Voorhees
Killer JSON for C++
Loading...
Searching...
No Matches
enum_adapter.hpp
Go to the documentation of this file.
1/// \file jsonv/serialization/enum_adapter.hpp
2///
3/// Copyright (c) 2015-2026 by Travis Gockel. All rights reserved.
4///
5/// This program is free software: you can redistribute it and/or modify it under the terms of the Apache License
6/// as published by the Apache Software Foundation, either version 2 of the License, or (at your option) any later
7/// version.
8///
9/// \author Travis Gockel (travis@gockelhut.com)
10#pragma once
11
12#include <jsonv/ast.hpp>
13#include <jsonv/config.hpp>
14#include <jsonv/functional.hpp>
15#include <jsonv/reader.hpp>
17
18#include <expected>
19#include <functional>
20#include <initializer_list>
21#include <map>
22#include <string>
23#include <string_view>
24#include <utility>
25
26#include "adapter_for.hpp"
27
28namespace jsonv
29{
30
31namespace detail
32{
33
34/// Where a \c kind::string holding some text sorts against a \c value under \c value_less.
35struct enum_text_order
36{
38 static int compare(const value& a, std::string_view b)
39 {
40 return compare_text(a, b);
41 }
42};
43
44/// Where a \c kind::string holding some text sorts against a \c value under \c value_less_icase.
45struct enum_text_order_icase
46{
48 static int compare(const value& a, std::string_view b)
49 {
50 return compare_text_icase(a, b);
51 }
52};
53
54/// Orders the JSON side of an \c enum_adapter mapping by \c FValueComp, and places the text of a string among it by
55/// \c TTextOrder -- which has to be where \c FValueComp would place a \c value holding that string, or a lookup could
56/// miss what the table holds. That is what lets a string read from JSON text be looked up without building a \c value
57/// to hold it.
58template <typename FValueComp, typename TTextOrder>
59struct enum_value_less
60{
61 using is_transparent = void;
62
64 bool operator()(const value& a, const value& b) const
65 {
66 return FValueComp()(a, b);
67 }
68
70 bool operator()(const value& a, std::string_view b) const
71 {
72 return TTextOrder::compare(a, b) < 0;
73 }
74
76 bool operator()(std::string_view a, const value& b) const
77 {
78 // Reversed rather than negated: a comparison may return INT_MIN, which has no negation.
79 return TTextOrder::compare(b, a) > 0;
80 }
81};
82
83/// How an \c enum_adapter orders its mapping, given the \c FValueComp it was asked for. An ordering the library does not
84/// know can only be asked about a \c value, so it is used as it is, and a string read from JSON text is built into one
85/// to be looked up. A transparent one is no exception: <tt>std::less&lt;&gt;</tt> cannot place a \c std::string_view
86/// before a \c value.
87template <typename FValueComp>
88struct enum_value_order
89{
90 using type = FValueComp;
91
92 static constexpr bool reads_text = false;
93};
94
95template <>
96struct enum_value_order<std::less<value>>
97{
98 using type = enum_value_less<std::less<value>, enum_text_order>;
99
100 static constexpr bool reads_text = true;
101};
102
103template <>
104struct enum_value_order<value_less>
105{
106 using type = enum_value_less<value_less, enum_text_order>;
107
108 static constexpr bool reads_text = true;
109};
110
111template <>
112struct enum_value_order<value_less_icase>
113{
114 using type = enum_value_less<value_less_icase, enum_text_order_icase>;
115
116 static constexpr bool reads_text = true;
117};
118
119}
120
121/// \addtogroup Serialization
122/// \{
123
124/// An adapter for enumeration types. The most common use of this is to map \c enum values in C++ to string values in a
125/// JSON representation (and vice versa).
126///
127/// Deserialization reads the \c reader directly. A value the mapping does not hold is refused with the reader still on
128/// it, and the problem names the value and lists every JSON value the mapping accepts, in the mapping's order:
129///
130/// \code
131/// Invalid value for ring: "bogus" (expected one of "earth", "fire", "heart", "useless", "water", "wind")
132/// \endcode
133///
134/// \tparam TEnum The type to map. This is not restricted to C++ enumerations (types defined with the \c enum keyword),
135/// but any type you wish to restrict to a subset of values.
136/// \tparam FEnumComp <tt>bool (*)(TEnum, TEnum)</tt> -- a strict ordering for \c TEnum values.
137/// \tparam FValueComp <tt>bool (*)(value, value)</tt> -- a strict ordering for \c value objects. By default, this is a
138/// case-sensitive comparison, but this can be replaced with anything you desire (for example, use
139/// \c value_less_icase to ignore case in deserializing from JSON). Under the library's own orderings
140/// -- <tt>std::less&lt;value&gt;</tt>, \c value_less and \c value_less_icase -- a string read from
141/// JSON text is looked up by its text, with no \c value built to hold it. Any other ordering can
142/// only be asked about a \c value, so one is built for every string deserialized from text.
143///
144/// \see enum_adapter_icase
145template <typename TEnum,
146 typename FEnumComp = std::less<TEnum>,
147 typename FValueComp = std::less<value>
148 >
150 public adapter_for<TEnum>
151{
152public:
153 /// Create an adapter with mapping values from the range <tt>[first, last)</tt>.
154 ///
155 /// \tparam TForwardIterator An iterator yielding the type <tt>std::pair&lt;TEnum, jsonv::value&gt;</tt>
156 template <typename TForwardIterator>
157 explicit enum_adapter(std::string enum_name, TForwardIterator first, TForwardIterator last) :
158 _enum_name(std::move(enum_name))
159 {
160 for (auto iter = first; iter != last; ++iter)
161 {
162 _val_to_cpp.insert({ iter->second, iter->first });
163 _cpp_to_val.insert(*iter);
164 }
165 }
166
167 /// Create an adapter with the specified \a mapping values.
168 ///
169 /// \param enum_name A user-friendly name for this enumeration to be used in error messages.
170 /// \param mapping A list of C++ types and values to use in \c to_json and \c deserialize. It is okay to have a C++
171 /// value with more than one JSON representation. In this case, the \e first JSON representation will
172 /// be used in \c to_json, but \e all JSON representations will be interpreted as the C++ value. It
173 /// is also okay to have the same JSON representation for multiple C++ values. In this case, the
174 /// \e first JSON representation provided for that value will be used in \c deserialize.
175 ///
176 /// For example:
177 ///
178 /// \code
179 /// enum_adapter<ring>("ring",
180 /// {
181 /// { ring::fire, "fire" },
182 /// { ring::wind, "wind" },
183 /// { ring::earth, "earth" },
184 /// { ring::water, "water" },
185 /// { ring::heart, "heart" }, // "heart" is preferred for to_json
186 /// { ring::heart, "useless" }, // "useless" is interpreted as ring::heart in deserialize
187 /// }
188 /// );
189 /// \endcode
190 explicit enum_adapter(std::string enum_name, std::initializer_list<std::pair<TEnum, value>> mapping) :
191 enum_adapter(std::move(enum_name), mapping.begin(), mapping.end())
192 { }
193
194protected:
196 virtual std::expected<TEnum, ast_node_type> create(deserialization_context& context, reader& from) const override
197 {
198 // A value-backed reader lends the value itself, which is looked up where it sits.
199 if (auto lent = from.current_value())
200 return settle(context, from, *lent);
201
202 if constexpr (value_order::reads_text)
203 {
204 // An unescaped string is looked up by a view of the source, so nothing is built for it. One written with
205 // escapes has no contiguous form to compare against until it is decoded.
206 if (from.good() && from.current_type() == ast_node_type::string_canonical)
207 return settle(context, from, from.current().as<ast_node::string_canonical>().value());
208
209 if (from.good() && from.current_type() == ast_node_type::string_escaped)
210 {
211 const std::string decoded = from.current().as<ast_node::string_escaped>().value();
212 return settle(context, from, std::string_view(decoded));
213 }
214 }
215
216 // Anything else is built as a `value` without moving the reader -- a scalar where it sits, a structure through a
217 // second cursor -- so a miss leaves this one on the value it is about.
218 return settle(context, from, detail::peek_value(context, from));
219 }
220
221 virtual void serialize(const serialization_context&, const TEnum& from, writer& to) const override
222 {
223 using std::end;
224
225 auto iter = _cpp_to_val.find(from);
226 if (iter != end(_cpp_to_val))
227 to.write(iter->second);
228 else
229 to.null();
230 }
231
232private:
233 using value_order = detail::enum_value_order<FValueComp>;
234
235 /// Look up \a key -- a \c value, or the text of a string -- and step \a from past the value it was read from.
236 template <typename TKey>
237 std::expected<TEnum, ast_node_type> settle(deserialization_context& context, reader& from, const TKey& key) const
238 {
239 auto iter = _val_to_cpp.find(key);
240 if (iter == _val_to_cpp.end())
241 {
242 // The cursor is still on the value, which is where a problem about it belongs and what whoever recovers
243 // from it expects to step over. Text is described as the string `value` it would have been, so the
244 // message spells it as JSON.
245 return context.problem(context.problem_path(from), describe_miss(value(key)));
246 }
247
248 // Copied while the cursor is still on the value, so a `TEnum` which refuses to be copied fails with the value in
249 // front of it. Moving it out is the last thing which can fail, and by then the value is behind the cursor.
250 TEnum out = iter->second;
251 (void) from.next_value();
252 try
253 {
254 return out;
255 }
256 catch (...)
257 {
258 context.note_value_consumed(from);
259 throw;
260 }
261 }
262
263 /// The message a miss on \a found is reported with: the value as JSON, then every one the mapping would accept.
264 std::string describe_miss(const value& found) const
265 {
266 std::string message = "Invalid value for " + _enum_name + ": " + to_string(found);
267 if (!_val_to_cpp.empty())
268 {
269 const char* separator = " (expected one of ";
270 for (const auto& accepted : _val_to_cpp)
271 {
272 message += separator;
273 message += to_string(accepted.first);
274 separator = ", ";
275 }
276 message += ')';
277 }
278 return message;
279 }
280
281private:
282 std::string _enum_name;
283 std::map<value, TEnum, typename value_order::type> _val_to_cpp;
284 std::map<TEnum, value, FEnumComp> _cpp_to_val;
285};
286
287/// An adapter for enumeration types which ignores the case when deserializing from JSON.
288///
289/// \see enum_adapter
290template <typename TEnum, typename FEnumComp = std::less<TEnum>>
292
293/// \}
294
295}
Copyright (c) 2015-2026 by Travis Gockel.
Utilities for directly dealing with a JSON AST.
An adapter for the type T.
const T & as() const
Get the underlying data of this node as T.
Definition ast.hpp:540
Provides extra information to routines used for deserialization and serialization.
Definition context.hpp:26
Provides extra information to routines used for deserialization, collects the problems they encounter...
void note_value_consumed(const reader &from) noexcept
Note that the failure about to be reported has already consumed, from from, the value it failed on,...
An adapter for enumeration types.
virtual std::expected< TEnum, ast_node_type > create(deserialization_context &context, reader &from) const override
Create an instance of T by reading from.
virtual void serialize(const serialization_context &, const TEnum &from, writer &to) const override
Write from into to.
enum_adapter(std::string enum_name, TForwardIterator first, TForwardIterator last)
Create an adapter with mapping values from the range [first, last).
enum_adapter(std::string enum_name, std::initializer_list< std::pair< TEnum, value > > mapping)
Create an adapter with the specified mapping values.
A reader instance reads from some form of JSON source (probably a string) and converts it into a JSON...
Definition reader.hpp:106
bool next_value() noexcept
Go to one past the value this reader is on.
ast_node_type current_type() const
Get the type of the current AST node, which is always current().type().
const ast_node & current() const
Get the current AST node this reader is pointing at.
optional< const value & > current_value() const noexcept
Get the in-memory value this reader is positioned on, if it has one to lend.
bool good() const
Check if this reader is still good to read from.
Provides extra information to routines used for serialization: the formats to find other serializers ...
Definition serialize.hpp:42
Represents a single JSON value, which can be any one of a potential kind, each behaving slightly diff...
Definition value.hpp:156
A writer instance writes a JSON ast_node sequence to some form of sink: an encoder,...
Definition writer.hpp:68
writer & write(const value &source)
Write the whole of source as one value, as encoder::encode does.
writer & null()
Write a scalar value.
Copyright (c) 2014-2020 by Travis Gockel.
A collection of function objects a la &lt;functional&gt;.
int compare(const value &a, const value &b, const TCompareTraits &traits)
Compare the values a and b using the comparison traits.
#define JSONV_NODISCARD
Warn if the caller discards the result of this function.
Definition config.hpp:143
JSONV_PUBLIC std::string to_string(const ast_node_type &type)
Get what operator<< writes for type as a std::string.
STL namespace.
Read a JSON AST.
Conversion between C++ types and JSON values.