userver: userver/formats/common/conversion_stack.hpp Source File
Loading...
Searching...
No Matches
conversion_stack.hpp
Go to the documentation of this file.
1#pragma once
2
3/// @file userver/formats/common/conversion_stack.hpp
4/// @brief @copybrief formats::common::ConversionStack
5
6#include <cstdint>
7#include <optional>
8#include <string>
9#include <type_traits>
10#include <utility>
11
12#include <fmt/format.h>
13#include <boost/container/deque.hpp>
14#include <boost/container/options.hpp>
15
16#include <userver/compiler/demangle.hpp>
17#include <userver/formats/common/type.hpp>
18#include <userver/utils/assert.hpp>
19
20USERVER_NAMESPACE_BEGIN
21
22/// @brief Shared types and helpers for JSON/YAML format layers.
23namespace formats::common {
24
25/// @brief Used in the implementation of functions that convert between
26/// different formats, e.g. formats::yaml::Value to formats::json::Value, etc.
27///
28/// Does not use recursion, so stack overflow will never happen with deeply
29/// nested or wide objects or arrays.
30///
31/// @tparam ValueFrom the Value type, FROM which we convert,
32/// e.g. formats::yaml::Value
33/// \tparam ValueToBuilder the ValueBuilder type, TO which we convert,
34/// e.g. formats::json::ValueBuilder
35///
36/// How to build the conversion function:
37/// 1. Start by constructing the ConversionStack from the source `ValueFrom`
38/// 2. While `!IsParsed`:
39/// 1. call `GetNextFrom`
40/// 2. analyse the kind of the current `ValueFrom`
41/// 3. call one of:
42/// - CastCurrentPrimitive
43/// - SetCurrent
44/// - EnterItems
45/// 3. Call `GetParsed`
46///
47/// See the implementation of PerformMinimalFormatConversion as an example.
48template <typename ValueFrom, typename ValueToBuilder>
49class ConversionStack final {
50public:
51 /// Start the conversion from `value`.
52 explicit ConversionStack(ValueFrom value)
53 : stack_()
54 {
55 stack_.emplace_back(std::move(value));
56 }
57
58 ConversionStack(ConversionStack&&) = delete;
59 ConversionStack& operator=(ConversionStack&&) = delete;
60
61 /// Check whether the whole source ValueFrom has been parsed.
62 bool IsParsed() const { return stack_.front().is_parsed; }
63
64 /// Get the parsing result, precondition: `IsParsed()`.
65 ValueToBuilder&& GetParsed() && {
67 return std::move(stack_.front().to).value();
68 }
69
70 /// Get the current sub-`ValueFrom` to convert.
71 const ValueFrom& GetNextFrom() const {
72 if (stack_.front().is_parsed) {
73 return stack_.front().from;
74 } else if (!stack_.back().is_parsed) {
75 return stack_.back().from;
76 } else {
77 return stack_[stack_.size() - 2].from;
78 }
79 }
80
81 /// Use for when it's discovered that the current `ValueFrom` type is
82 /// available and has the same semantics in ValueFrom and ValueToBuilder,
83 /// e.g. `std::int64_t`, `std::string`, etc.
84 template <typename T>
86 UASSERT(!stack_.front().is_parsed && !stack_.back().is_parsed);
87 SetCurrent(stack_.back().from.template As<T>());
88 }
89
90 /// Use for when it's discovered that the current `ValueFrom` type is not
91 /// directly convertible to `ValueToBuilder`, but can be converted manually
92 /// (this might be an inexact conversion). For example, ValueFrom could have a
93 /// special representation for datetime, and we manually convert it to string
94 /// using some arbitrary format.
95 template <typename T>
96 void SetCurrent(T&& value) {
97 stack_.back().to.emplace(std::forward<T>(value));
98 stack_.back().is_parsed = true;
99 }
100
101 /// Use for when an object or an array `ValueFrom` is encountered.
102 void EnterItems() {
103 if (!stack_.back().is_parsed) {
104 if (!stack_.back().to) {
105 if (stack_.back().from.IsObject()) {
106 stack_.back().to.emplace(common::Type::kObject);
107 } else if (stack_.back().from.IsArray()) {
108 stack_.back().to.emplace(common::Type::kArray);
109 } else {
110 UINVARIANT(false, "Type mismatch");
111 }
112 }
113 if (stack_.back().current_parsing_elem) {
114 ++*stack_.back().current_parsing_elem;
115 } else {
116 stack_.back().current_parsing_elem = stack_.back().from.begin();
117 }
118 if (stack_.back().current_parsing_elem.value() == stack_.back().from.end()) {
119 stack_.back().is_parsed = true;
120 return;
121 }
122 auto new_from = *stack_.back().current_parsing_elem.value();
123 stack_.emplace_back(std::move(new_from));
124 return;
125 }
126 UASSERT(stack_.size() > 1);
127 auto& current_docs = stack_[stack_.size() - 2];
128 if (current_docs.from.IsObject()) {
129 current_docs.to.value()[current_docs.current_parsing_elem->GetName()] = std::move(stack_.back().to.value());
130 } else if (current_docs.from.IsArray()) {
131 current_docs.to->PushBack(std::move(stack_.back().to.value()));
132 } else {
133 UASSERT_MSG(false, "Type mismatch");
134 }
135 stack_.pop_back();
136 }
137
138private:
139 struct StackFrame final {
140 explicit StackFrame(ValueFrom&& from)
141 : from(std::move(from))
142 {}
143 explicit StackFrame(const ValueFrom& from)
144 : from(from)
145 {}
146
147 const ValueFrom from;
148 std::optional<ValueToBuilder> to{};
149 std::optional<typename ValueFrom::const_iterator> current_parsing_elem{};
150 bool is_parsed{false};
151 };
152
153 // The container must have stable references. For example, YamlConfig::const_iterator stores a pointer
154 // to the YamlConfig node that is being iterated over. If a stack frame moves, then the next frame's
155 // `current_parsing_elem` will be invalidated. Updating iterators in such cases would be complex and expensive.
156 boost::container::deque<StackFrame, void, boost::container::deque_options<boost::container::block_size<16>>::type>
157 stack_;
158};
159
160/// @brief Performs the conversion between different formats. Only supports
161/// basic formats node types, throws on any non-standard ones.
162///
163/// @note This is intended as a building block for conversion functions. Prefer
164/// calling `value.As<AnotherFormat>()` or `value.ConvertTo<AnotherFormat>()`.
165template <typename ValueTo, typename ValueFrom>
166ValueTo PerformMinimalFormatConversion(ValueFrom&& value) {
167 if (value.IsMissing()) {
168 throw typename std::decay_t<ValueFrom>::Exception(fmt::format(
169 "Failed to convert value at '{}' from {} to {}: missing value",
170 value.GetPath(),
171 compiler::GetTypeName<std::decay_t<ValueFrom>>(),
172 compiler::GetTypeName<ValueTo>()
173 ));
174 }
175 formats::common::ConversionStack<std::decay_t<ValueFrom>, typename ValueTo::Builder>
176 conversion_stack(std::forward<ValueFrom>(value));
177 while (!conversion_stack.IsParsed()) {
178 const auto& from = conversion_stack.GetNextFrom();
179 if (from.IsBool()) {
180 conversion_stack.template CastCurrentPrimitive<bool>();
181 } else if (from.IsInt()) {
182 conversion_stack.template CastCurrentPrimitive<int>();
183 } else if (from.IsInt64()) {
184 conversion_stack.template CastCurrentPrimitive<std::int64_t>();
185 } else if (from.IsUInt64()) {
186 conversion_stack.template CastCurrentPrimitive<std::uint64_t>();
187 } else if (from.IsDouble()) {
188 conversion_stack.template CastCurrentPrimitive<double>();
189 } else if (from.IsString()) {
190 conversion_stack.template CastCurrentPrimitive<std::string>();
191 } else if (from.IsNull()) {
192 conversion_stack.SetCurrent(common::Type::kNull);
193 } else if (from.IsArray() || from.IsObject()) {
194 conversion_stack.EnterItems();
195 } else {
196 throw typename std::decay_t<ValueFrom>::Exception(fmt::format(
197 "Failed to convert value at '{}' from {} to {}: unknown node type",
198 from.GetPath(),
199 compiler::GetTypeName<std::decay_t<ValueFrom>>(),
200 compiler::GetTypeName<ValueTo>()
201 ));
202 }
203 }
204 return std::move(conversion_stack).GetParsed().ExtractValue();
205}
206
207} // namespace formats::common
208
209USERVER_NAMESPACE_END