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