MUSX Document Model
Loading...
Searching...
No Matches
FactoryBase.h
1/*
2 * Copyright (C) 2025, Robert Patterson
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a copy
5 * of this software and associated documentation files (the "Software"), to deal
6 * in the Software without restriction, including without limitation the rights
7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 * copies of the Software, and to permit persons to whom the Software is
9 * furnished to do so, subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included in
12 * all copies or substantial portions of the Software.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 * THE SOFTWARE.
21 */
22#pragma once
23
24#include <stdexcept>
25#include <string>
26#include <string_view>
27#include <optional>
28#include <functional>
29#include <unordered_set>
30#include <unordered_map>
31#include <map>
32#include <tuple>
33#include <sstream>
34
35#include "musx/util/Logger.h"
36#include "musx/xml/XmlInterface.h"
37#include "musx/dom/BaseClasses.h"
38#include "musx/dom/Document.h"
39
40namespace musx {
41
46namespace factory {
47
48using namespace musx::xml;
49using namespace musx::dom;
50
51
55class unknown_xml_error : public std::runtime_error
56{
57public:
58 using std::runtime_error::runtime_error;
59};
60
61
71public:
79 using Resolver = std::function<void(const dom::DocumentPtr&)>;
80
90 void addResolver(Resolver resolver, const std::string_view& key)
91 {
92 assert(!key.empty());
93 if (resolvers.count(std::string(key)) > 0) {
94 return; // Skip if already registered
95 }
96
97 resolvers[std::string(key)] = std::move(resolver);
98 }
99
108 void resolveAll(const dom::DocumentPtr& document)
109 {
110 for (const auto& [key, resolver] : resolvers) {
111 resolver(document);
112 }
113
114 resolvers.clear();
115 }
116
117private:
124 std::map<std::string, Resolver> resolvers;
125};
126
127
132{
133protected:
144 template<typename DataType, typename ParserFunc>
145 static void getFieldFromXml(const XmlElementPtr& element, const std::string& nodeName, DataType& dataField, ParserFunc parserFunc, bool expected = false)
146 {
147 if (auto childElement = element->getFirstChildElement(nodeName)) {
148 dataField = parserFunc(childElement);
149 } else if (expected) {
150 std::stringstream msg;
151 msg << "Expected field <" << element->getTagName() << "><" << nodeName << "> not found.";
153 }
154 }
155
160 static XmlElementPtr getFirstChildElement(const XmlElementPtr& element, const std::string& childElementName)
161 {
162 auto childElement = element->getFirstChildElement(childElementName);
163 if (!childElement) {
164 throw std::invalid_argument("Missing <" + childElementName + "> element.");
165 }
166 return childElement;
167 }
168
170 static std::optional<std::string> getOptionalChildText(const XmlElementPtr& element, const std::string& childElementName)
171 {
172 auto childElement = element->getFirstChildElement(childElementName);
173 if (!childElement) {
174 return std::nullopt;
175 }
176 return childElement->getText();
177 }
178
180 template<typename T>
181 static std::optional<T> getOptionalChildTextAs(const XmlElementPtr& element, const std::string& childElementName, T defaultValue = {})
182 {
183 auto childElement = element->getFirstChildElement(childElementName);
184 if (!childElement) {
185 return std::nullopt;
186 }
187 return childElement->getTextAs<T>(defaultValue);
188 }
189
190public:
191 virtual ~FactoryBase() {}
192};
193
194#ifndef DOXYGEN_SHOULD_IGNORE_THIS
195
196template <typename EnumClass, typename FromClass = std::string_view>
197using XmlEnumMappingElement = std::unordered_map<FromClass, EnumClass>;
198template <typename EnumClass, typename FromClass = std::string_view>
199struct XmlEnumMapping
200{
201 static const XmlEnumMappingElement<EnumClass, FromClass> mapping;
202};
203
204#define MUSX_XML_ENUM_MAPPING(Type, ...) \
205template <> \
206const XmlEnumMappingElement<Type> XmlEnumMapping<Type>::mapping = __VA_ARGS__
207
208template <typename EnumClass, bool IgnoreUnknown, typename FromClass = std::string_view>
209class EnumMapper
210{
211 // If we ever need to, we can create a static lazy-initialize reverse mapping function here
212
213public:
214 static EnumClass xmlToEnum(const FromClass& value)
215 {
216 auto it = XmlEnumMapping<EnumClass>::mapping.find(value);
217 if (it != XmlEnumMapping<EnumClass>::mapping.end()) {
218 return it->second;
219 }
220 if constexpr (!IgnoreUnknown) {
221 std::string msg = [value]() {
222 if constexpr (std::is_arithmetic_v<FromClass>) {
223 return "Invalid enum value from xml: " + std::to_string(value);
224 }
225 else {
226 return "Invalid enum value from xml: " + std::string(value);
227 }
228 }();
229 MUSX_UNKNOWN_XML(msg);
230 }
231 return {};
232 }
233};
234
235template<typename EnumClass, typename FromClass, bool IgnoreUnknown = false>
236EnumClass toEnum(const FromClass& value)
237{
238 if constexpr (std::is_convertible_v<FromClass, std::string_view>) {
239 return EnumMapper<EnumClass, IgnoreUnknown, std::string_view>::xmlToEnum(value);
240 } else {
241 return EnumMapper<EnumClass, IgnoreUnknown, FromClass>::xmlToEnum(value);
242 }
243}
244
245template<typename EnumClass, bool IgnoreUnknown = false>
246EnumClass toEnum(const ::musx::xml::XmlElementPtr& e)
247{
248 return toEnum<EnumClass, std::string_view, IgnoreUnknown>(e->getTextTrimmed());
249}
250
251#define MUSX_XML_ELEMENT_ARRAY(Type, ...) \
252const ::musx::xml::XmlElementArray<Type>& Type::xmlMappingArray() { \
253 static const ::musx::xml::XmlElementArray<Type> instance = __VA_ARGS__; \
254 return instance; \
255} \
256static_assert(true, "") // require semicolon after macro
257
258using ResolverEntry = std::optional<ElementLinker::Resolver>;
259template <typename T>
260struct ResolverContainer
261{
262 inline static const ResolverEntry resolver = {};
263};
264
265#define MUSX_RESOLVER_ENTRY(Type, ...) \
266template <> \
267struct ResolverContainer<Type> { \
268 inline static const ResolverEntry resolver = ElementLinker::Resolver(__VA_ARGS__); \
269};
270
271template <typename T>
272struct FieldPopulator : public FactoryBase
273{
274 static void populateField(const std::shared_ptr<T>& instance, const XmlElementPtr& fieldElement)
275 {
276 auto it = elementXref().find(fieldElement->getTagName());
277 if (it != elementXref().end()) {
278 std::get<1>(*it)(fieldElement, instance);
279 } else {
280 const bool requireFields = [instance]() {
281 if constexpr (std::is_base_of_v<Base, T>) {
282 return instance->requireAllFields();
283 } else {
284 return true;
285 }
286 }();
287 if (requireFields) {
288 MUSX_UNKNOWN_XML("xml element <" + fieldElement->getParent()->getTagName() + "> has child <" + fieldElement->getTagName() + "> which is not in the element list.");
289 }
290 }
291 }
292
293 static void populate(const std::shared_ptr<T>& instance, const XmlElementPtr& element)
294 {
295 if constexpr (std::is_base_of_v<TextsBase, T>) {
296 instance->text = element->getText();
297 } else {
298 for (auto child = element->getFirstChildElement(); child; child = child->getNextSibling()) {
299 populateField(instance, child);
300 }
301 }
302 if constexpr (std::is_base_of_v<Base, T>) {
303 instance->integrityCheck();
304 }
305 }
306
307 static void populate(const std::shared_ptr<T>& instance, const XmlElementPtr& element, ElementLinker& elementLinker)
308 {
309 populate(instance, element);
310 if (ResolverContainer<T>::resolver.has_value()) {
311 elementLinker.addResolver(ResolverContainer<T>::resolver.value(), element->getTagName());
312 }
313 }
314
315 template <typename... Args>
316 static std::shared_ptr<T> createAndPopulate(const XmlElementPtr& element, Args&&... args)
317 {
318 return FieldPopulator<T>::createAndPopulateImpl(element, std::forward<Args>(args)...);
319 }
320
321private:
322 static const std::unordered_map<std::string_view, XmlElementPopulator<T>>& elementXref()
323 {
324 static const std::unordered_map<std::string_view, XmlElementPopulator<T>> xref = []()
325 {
326 std::unordered_map<std::string_view, XmlElementPopulator<T>> retval;
327 auto mappingArray = T::xmlMappingArray();
328 for (std::size_t i = 0; i < mappingArray.size(); i++) {
329 const XmlElementDescriptor<T> descriptor = mappingArray[i];
330 retval[std::get<0>(descriptor)] = std::get<1>(descriptor);
331 }
332 return retval;
333 }();
334 return xref;
335 }
336
337 template <typename... Args>
338 static std::shared_ptr<T> createAndPopulateImpl(const XmlElementPtr& element, Args&&... args)
339 {
340 auto instance = std::make_shared<T>(std::forward<Args>(args)...);
341 FieldPopulator<T>::populate(instance, element);
342 return instance;
343 }
344};
345
351template <typename EnumClass, typename EmbeddedClass>
352static void populateEmbeddedClass(const XmlElementPtr& e, std::unordered_map<EnumClass, std::shared_ptr<EmbeddedClass>>& listArray)
353{
354 auto typeAttr = e->findAttribute("type");
355 if (!typeAttr) {
356 throw std::invalid_argument("<" + e->getTagName() + "> element has no type attribute");
357 }
358 listArray.emplace(toEnum<EnumClass>(typeAttr->getValueTrimmed()), FieldPopulator<EmbeddedClass>::createAndPopulate(e));
359}
360
366template <typename T>
367static std::vector<T> populateEmbeddedArray(const XmlElementPtr& e, const std::string_view& elementNodeName)
368{
369 std::vector<T> result;
370 for (auto child = e->getFirstChildElement(); child; child = child->getNextSibling()) {
371 if (child->getTagName() != elementNodeName) {
372 MUSX_UNKNOWN_XML("Unknown tag <" + child->getTagName() + "> while processing embedded xml array <" + e->getTagName() + ">");
373 continue;
374 }
375 if constexpr (std::is_fundamental_v<T>) {
376 result.push_back(child->getTextAs<T>());
377 } else if constexpr (std::is_same_v<T, std::string>) {
378 result.push_back(child->getText());
379 } else {
380 result.push_back(FieldPopulator<T>::createAndPopulate(child));
381 }
382 }
383 return result;
384}
385
386template <>
387template <typename... Args>
388inline std::shared_ptr<FontInfo> FieldPopulator<FontInfo>::createAndPopulate(const XmlElementPtr& element, Args&&... args)
389{
390 if (!element->getFirstChildElement()) return nullptr;
391 return FieldPopulator<FontInfo>::createAndPopulateImpl(element, std::forward<Args>(args)...);
392}
393
394void populateFontEfx(const XmlElementPtr& e, const std::shared_ptr<dom::FontInfo>& i);
395
396#endif // DOXYGEN_SHOULD_IGNORE_THIS
397
398} // namespace factory
399} // namespace musx
A utility class for managing deferred relationships between elements during document construction.
Definition FactoryBase.h:70
std::function< void(const dom::DocumentPtr &)> Resolver
A callable type representing a deferred relationship resolver.
Definition FactoryBase.h:79
void resolveAll(const dom::DocumentPtr &document)
Resolves all deferred relationships.
Definition FactoryBase.h:108
void addResolver(Resolver resolver, const std::string_view &key)
Adds a resolver function to the linker.
Definition FactoryBase.h:90
Factory base class.
Definition FactoryBase.h:132
static void getFieldFromXml(const XmlElementPtr &element, const std::string &nodeName, DataType &dataField, ParserFunc parserFunc, bool expected=false)
Helper function to check if a child exists and populate it if so.
Definition FactoryBase.h:145
static XmlElementPtr getFirstChildElement(const XmlElementPtr &element, const std::string &childElementName)
Helper function to throw when child element does not exist.
Definition FactoryBase.h:160
static std::optional< T > getOptionalChildTextAs(const XmlElementPtr &element, const std::string &childElementName, T defaultValue={})
Helper function to return std::nullopt when child element does not exist.
Definition FactoryBase.h:181
static std::optional< std::string > getOptionalChildText(const XmlElementPtr &element, const std::string &childElementName)
Helper function to return std::nullopt when child element does not exist.
Definition FactoryBase.h:170
Exception for unknown xml node errors. (Used when MUSX_THROW_ON_UNKNOWN_XML is defined....
Definition FactoryBase.h:56
@ Warning
Warning messages indicating potential issues.
static void log(LogLevel level, const std::string &message)
Logs a message with a specific severity level.
Definition Logger.h:87
The DOM (document object model) for musx files.
std::shared_ptr< Document > DocumentPtr
Shared Document pointer.
Definition BaseClasses.h:55
Provides interfaces and optional implementations for traversing XML.
Definition PugiXmlImpl.h:33
std::tuple< const std::string_view, XmlElementPopulator< T > > XmlElementDescriptor
associates an xml node name with and XmlElementPopulator
Definition XmlInterface.h:125
std::shared_ptr< IXmlElement > XmlElementPtr
shared pointer to IXmlElement
Definition XmlInterface.h:121
object model for musx file (enigmaxml)
Definition BaseClasses.h:32