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