diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..5a7d3e1 --- /dev/null +++ b/.clang-format @@ -0,0 +1,143 @@ +--- +IndentWidth: 4 + +AlignAfterOpenBracket: BlockIndent +AlignEscapedNewlines: Left +AlignConsecutiveAssignments: false +AlignOperands: DontAlign +AlignTrailingComments: false + +AllowAllArgumentsOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: false + +AllowShortBlocksOnASingleLine: Empty +AllowShortFunctionsOnASingleLine: Empty +AllowShortLambdasOnASingleLine: Empty + +AllowShortCaseLabelsOnASingleLine: true +AllowShortEnumsOnASingleLine: false + +AllowShortIfStatementsOnASingleLine: AllIfsAndElse +AlwaysBreakAfterReturnType: None + +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: Yes + +BinPackArguments: false +BinPackParameters: true + +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: Never + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + + BeforeCatch: true + BeforeElse: true + + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: false + +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Custom +BreakBeforeConceptDeclarations: true +BreakBeforeTernaryOperators: false +BreakConstructorInitializers: AfterColon +BreakInheritanceList: AfterColon +BreakStringLiterals: true +ColumnLimit: 100 +CompactNamespaces: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true + +DeriveLineEnding: false +UseCRLF: false +UseTab: Never + +PointerAlignment: Left +ReferenceAlignment: Pointer + +EmptyLineBeforeAccessModifier: Always +FixNamespaceComments: false + +IncludeBlocks: Regroup +IncludeCategories: +- Regex: '^".*"' + Priority: 1 + +- Regex: '^<.*>' + Priority: 2 + +IndentAccessModifiers: false +AccessModifierOffset: -4 +IndentCaseBlocks: false +IndentCaseLabels: true +IndentExternBlock: Indent +IndentGotoLabels: true +IndentPPDirectives: BeforeHash +IndentRequiresClause: true +IndentWrappedFunctionNames: false +# InsertBraces: true +InsertTrailingCommas: None + +KeepEmptyLinesAtTheStartOfBlocks: false +MaxEmptyLinesToKeep: 1 +LambdaBodyIndentation: Signature +NamespaceIndentation: All + +PPIndentWidth: -1 + +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakOpenParenthesis: 0 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 10 +PenaltyReturnTypeOnItsOwnLine: 1000000 +PenaltyIndentedWhitespace: 0 + +QualifierAlignment: Right + +RequiresClausePosition: OwnLine +ReflowComments: true +SeparateDefinitionBlocks: Always + +SortIncludes: CaseSensitive +SortUsingDeclarations: true + +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceAroundPointerQualifiers: Default +SpaceBeforeAssignmentOperators: true +SpaceBeforeCaseColon: false +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceBeforeSquareBrackets: false +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: Never +SpacesInCStyleCastParentheses: false +SpacesInConditionalStatement: false +SpacesInLineCommentPrefix: + Minimum: 1 + Maximum: -1 +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: c++20 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ce873b1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.yml] +indent_size = 2 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8368373..844296d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,6 +26,13 @@ jobs: # - run: sudo apt install ninja-build - uses: seanmiddleditch/gha-setup-ninja@v4 + - name: Install GCC 12 + if: matrix.config.os == 'ubuntu-latest' + run: | + sudo apt install -y gcc-12 g++-12 + sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 100 + sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 100 + - name: Configure CMake run: cmake -B ${{github.workspace}}/build -G Ninja diff --git a/.gitignore b/.gitignore index 7f8d2fb..93483e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ build/ +build-*/ .vscode/ -.cache/ \ No newline at end of file +.cache/ diff --git a/CMakeLists.txt b/CMakeLists.txt index efbda5c..63ef73f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,12 +1,21 @@ cmake_minimum_required(VERSION 3.21) -project(mat-json VERSION 2.1.0) +project(mat-json VERSION 3.0.0) set(SOURCE_FILES - src/json.cpp src/external/dragonbox.cpp + src/value.cpp + src/impl.cpp + src/parser.cpp + src/dump.cpp ) +if (NOT COMMAND CPMAddPackage) + include(cmake/get_cpm.cmake) +endif() + +CPMAddPackage("gh:geode-sdk/result@1.1.1") + # This option is only useful for Geode itself if (DEFINED MAT_JSON_AS_INTERFACE AND MAT_JSON_AS_INTERFACE) add_library(mat-json INTERFACE) @@ -19,10 +28,19 @@ else() add_library(mat-json ${SOURCE_FILES}) target_compile_features(mat-json PUBLIC cxx_std_20) - + target_include_directories(mat-json PUBLIC include) + target_link_libraries(mat-json PUBLIC GeodeResult) endif() if (PROJECT_IS_TOP_LEVEL) + if (NOT WIN32) + add_compile_options("$<$:-fsanitize=address,undefined>") + add_link_options("$<$:-fsanitize=address,undefined>") + endif() + target_compile_options(mat-json PRIVATE + $<$:/W4> + $<$>:-Wall -Wextra -Wpedantic -Wno-gnu-statement-expression-from-macro-expansion> + ) add_subdirectory(test) endif() diff --git a/LICENSE b/LICENSE index 425828f..36b7cd9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,23 @@ -MIT License +Boost Software License - Version 1.0 - August 17th, 2003 -Copyright (c) 2023 mat +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 67f874f..11d1ac4 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,13 @@ # MATjson -json library i made quickly for geode. preserves insertion order for objects - -- this was made to be easy to compile -- numbers are internally stored as double -- not made to handle extremely nested objects +JSON library made for [Geode](https://github.com/geode-sdk/json). Main feature is preserving insertion order, along with the use of [Geode's Result library](https://github.com/geode-sdk/result). ## Example ```cpp #include -auto object = matjson::parse(R"( +auto result = matjson::parse(R"( { "hello": "world", "foo": { @@ -19,38 +15,105 @@ auto object = matjson::parse(R"( } } )"); +if (!result) { + println("Failed to parse json: {}", result.unwrapErr()); +} +matjson::Value object = result.unwrap(); -object["hello"].as_string(); // world -object["foo"]["bar"].as_int(); // 2000 -object.is_object(); // true -object.is_string(); // false +object["hello"].asString().unwrap(); // world +object["foo"]["bar"].asInt().unwrap(); // 2000 +object.isObject(); // true +object.isString(); // false ``` +# Index + +* [Usage](#usage) + * [Parsing JSON](#parsing-json) + * [Dumping JSON](#dumping-json) + * [Accessing object properties](#accessing-object-properties) + * [Serializing custom types](#serialzing-custom-types) + * [Objects and arrays](#objects-and-arrays) + * [Reflection](#reflection) + * [Support for standard library types](#support-for-standard-library-types) + # Usage -## Loading from a file -For the sake of simplicity, the library can only parse from a string, so you will have to load the whole file in memory yourself. +The main class of the library is `matjson::Value`, which can represent any JSON value. You can access its type by using the `type()` method, or one of the `isString()` methods. +```cpp +matjson::Value value = ...; + +matjson::Type type = value.type(); +if (type == matjson::Type::Object) { + // ... +} +// same as +if (value.isObject()) { + // ... +} +``` +Anything not documented here can be found in the [matjson.hpp](https://github.com/geode-sdk/json/blob/main/include/matjson.hpp) header, which is also well documented. + +## Parsing JSON + +To parse JSON into a `matjson::Value`, you can simply use the `matjson::parse` method + +```cpp +Result matjson::parse(std::string_view str); +Result matjson::parse(std::istream& stream); +``` ```cpp -std::string file_contents = ... +GEODE_UNWRAP_INTO(matjson::Value value, matjson::parse("[1, 2, 3, 4]")); -auto object = matjson::parse(file_contents); +std::ifstream file("file.json"); +GEODE_UNWRAP_INTO(matjson::Value value2, matjson::parse(file)); ``` -## Saving to a file -The library can also only directly output a string. +## Dumping JSON + +To convert a `matjson::Value` back to a JSON string, use `dump()` +```cpp +/// Dumps the JSON value to a string, with a given indentation. +/// If the given indentation is matjson::NO_INDENTATION, the json is compacted. +/// If the given indentation is matjson::TAB_INDENTATION, the json is indented with tabs. +/// @param indentationSize The number of spaces to use for indentation +/// @return The JSON string +/// @note Due to limitations in the JSON format, NaN and Infinity float values get converted to null. +/// This behavior is the same as many other JSON libraries. +std::string Value::dump(int indentation = 4); +``` ```cpp -matjson::Value object = ... +matjson::Value object = matjson::makeObject({ + {"x", 10} +}); -std::string file_contents = object.dump(); -write_to_file("epic.json", file_contents); +std::string jsonStr = object.dump(); +// { +// "x": 10 +// } + +std::string compact = object.dump(matjson::NO_INDENTATION); +// {"x":10} ``` `Value::dump` can also control indentation, see [Dumping to string](#dumping-to-string) -## Accessing values -There are many different ways to access inner values, depending on your needs: +## Accessing object properties + +There are many different ways to access inner properties ```cpp -auto object = matjson::parse(R"( +Result Value::get(std::string_view key); +Result Value::get(std::size_t index); +// These are special! +Value& Value::operator[](std::string_view key); +Value& Value::operator[](std::size_t index); +``` +As noted, `operator[]` is special because: +* If the `matjson::Value` is not const, `operator[](std::string_view key)` will insert into the object, akin to `std::map`. \ +(This does not apply if the value is an array) +* Otherwise, trying to access missing values will return a special `null` JSON value. This value cannot be mutated. +```cpp +matjson::Value object = matjson::parse(R"( { "hello": "world", "foo": { @@ -58,22 +121,13 @@ auto object = matjson::parse(R"( "numbers": [123, false, "not a number actually"] } } -)"); - -object["hello"].as_string(); // "world" -object["hello"].as(); // "world" -object.get("hello"); // "world" +)").unwrap(); -object["foo"]["bar"].as_int(); // 2000 -object["foo"]["bar"].as(); // 2000 -object["foo"].get("bar"); // 2000 +object["hello"]; // matjson::Value("world") +object.get("hello"); // Ok(matjson::Value("world")) -object["foo"]["numbers"][0].as_int(); // 123 -object["foo"]["numbers"][0].as(); // 123 -object["foo"]["numbers"].get(0); // 123 +object["foo"]["bar"].asInt(); // Ok(2000) -object.try_get("foo"); // std::optional(...) -object.try_get("sup"); // std::nullopt object.contains("sup"); // false // will insert "sup" into object as long as its not const. @@ -81,35 +135,34 @@ object.contains("sup"); // false object["sup"]; object["nya"] = 123; -object["hello"].is_string(); // true -object["hello"].is(); // true +// not the case for .get +object.get("something else"); // Err("key not found") ``` -## Custom types -It is possible to (de)serialize your own types into json, by specializing `matjson::Serialize`. +## Serializing custom types + +It's possible to (de)serialize your own types into json, by specializing `matjson::Serialize`. ```cpp +#include + struct User { std::string name; int age; -} +}; template <> struct matjson::Serialize { - static User from_json(const matjson::Value& value) { - return User { - .name = value["name"].as_string(), - .age = value["age"].as_int(), - }; + static geode::Result fromJson(const matjson::Value& value) { + GEODE_UNWRAP_INTO(std::string name, value["name"].asString()); + GEODE_UNWRAP_INTO(int age, value["age"].asInt()); + return geode::Ok(User { name, age }); } - static matjson::Value to_json(const User& user) { - return matjson::Object { + static matjson::Value toJson(const User& user) { + return matjson::makeObject({ { "name", user.name }, { "age", user.age } - }; + }); } - // You can also implement this method: - // > static bool is_json(const matjson::Value& value); - // It is only used if you do value.is(); }; int main() { @@ -123,48 +176,120 @@ int main() { value["name"] = "hello"; // you have to use the templated methods for accessing custom types! // it will *not* implicitly convert to User - User user2 = value.as(); + User user2 = value.as().unwrap(); user2.name; // "hello" } ``` -## Dumping to string -To save a json value to a file or to pretty print it you can use the `matjson::Value::dump` method. +Serialization is available for many standard library containers in `#include `, as mentioned in [Support for standard library types](#support-for-standard-library-types). + +There is also experimental support for [reflection](#reflection). + +## Objects and arrays + +Objects and arrays have special methods, since they can be iterated and mutated. ```cpp -// from the previous section -matjson::Value value = User { "mat", 123 }; +// Does nothing is Value is not an object +void Value::set(std::string_view key, Value value); +// Does nothing if Value is not an array +void Value::push(Value value); +// Clears all entries from an array or object +void Value::clear(); +// Does nothing if Value is not an object +bool Value::erase(std::string_view key); +// Returns 0 if Value is not an object or array +std::size_t Value::size() const; +``` +To make objects you can use the `matjson::makeObject` function, and for arrays you can use the `Value::Value(std::vector)` constructor. +```cpp +matjson::Value obj = matjson::makeObject({ + {"hello", "world"}, + {"number", "123"} +}); + +for (auto& [key, value] : obj) { + // key is std::string + // value is matjson::Value +} -// by default dumps with 4 spaces -value.dump(); -// { -// "name": "mat", -// "age": 123 -// } +// just iterates through the values +for (auto& value : obj) { + // value is matjson::Value +} -value.dump(1); // 1 space indentation -// { -// "name": "mat", -// "age": 123 -// } +obj.set("foo", true); -// will create a compact json with no extra whitepace -value.dump(matjson::NO_INDENTATION); -value.dump(0); // same as above -// {"name":"mat","age":123} +matjson::Value arr({ 1, 2, "hello", true }); -value.dump(matjson::TAB_INDENTATION); // will indent with tabs -// like imagine '{\n\t"name": "mat",\n\t"age": 123\n}' +for (auto& value : obj) { + // values +} + +arr.push(nullptr); +arr.dump(matjson::NO_INDENTATION); // [1,2,"hello",true,null] + +// If you need direct access to the vector, for some reason +std::vector& vec = arr.asArray().unwrap(); ``` -## misc +## Reflection + +The library now has experimental support for reflection using the [qlibs/reflect](https://github.com/qlibs/reflect) library, which only supports [aggregate types](https://en.cppreference.com/w/cpp/language/aggregate_initialization). + +Including the optional header will define JSON serialization for any type it can, making them able to be used with matjson instantly + ```cpp -// default constructs to a json object, for convenience -matjson::Value value; +#include + +struct Stats { + int hunger; + int health; +}; -// null is nullptr -value["hello"] = nullptr; -value["hello"].is_null(); // true +struct User { + std::string name; + Stats stats; + bool registered; +}; + +int main() { + User user = User { + .name = "Joe", + .stats = Stats { + .hunger = 0, + .health = 100 + }, + .registered = true + }; + + matjson::Value value = user; + + value.dump(); + // { + // "name": "Joe", + // "stats": { + // "hunger": 0, + // "health": 0 + // }, + // "registered": true + // } + + User u2 = value.as().unwrap(); +} ``` -## ok thats like everything -just look at the main header for other things its not that hard to read the code +## Support for standard library types + +There is built in support for serializing std containers by including an optional header: + +```cpp +#include + +std::vector nums = { 1, 2, 3 }; +std::map map = { + { "hello", "foo" } +}; + +matjson::Value jsonNums = nums; +matjson::Value jsonMap = map; +``` diff --git a/cmake/get_cpm.cmake b/cmake/get_cpm.cmake new file mode 100644 index 0000000..baf2d8c --- /dev/null +++ b/cmake/get_cpm.cmake @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: MIT +# +# SPDX-FileCopyrightText: Copyright (c) 2019-2023 Lars Melchior and contributors + +set(CPM_DOWNLOAD_VERSION 0.40.2) +set(CPM_HASH_SUM "c8cdc32c03816538ce22781ed72964dc864b2a34a310d3b7104812a5ca2d835d") + +if(CPM_SOURCE_CACHE) + set(CPM_DOWNLOAD_LOCATION "${CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +elseif(DEFINED ENV{CPM_SOURCE_CACHE}) + set(CPM_DOWNLOAD_LOCATION "$ENV{CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +else() + set(CPM_DOWNLOAD_LOCATION "${CMAKE_BINARY_DIR}/cmake/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +endif() + +# Expand relative path. This is important if the provided path contains a tilde (~) +get_filename_component(CPM_DOWNLOAD_LOCATION ${CPM_DOWNLOAD_LOCATION} ABSOLUTE) + +file(DOWNLOAD + https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake + ${CPM_DOWNLOAD_LOCATION} EXPECTED_HASH SHA256=${CPM_HASH_SUM} +) + +include(${CPM_DOWNLOAD_LOCATION}) diff --git a/include/matjson.hpp b/include/matjson.hpp index c5e7453..4e7f1fb 100644 --- a/include/matjson.hpp +++ b/include/matjson.hpp @@ -1,296 +1,411 @@ #pragma once -#include -#include +#include +#include #include +#include #include -#include -#include #ifdef MAT_JSON_DYNAMIC - #if defined(_WIN32) && !defined(__CYGWIN__) - #ifdef MAT_JSON_EXPORTING - #define MAT_JSON_DLL __declspec(dllexport) - #else - #define MAT_JSON_DLL __declspec(dllimport) - #endif - #else - #ifdef MAT_JSON_EXPORTING - #define MAT_JSON_DLL [[gnu::visibility("default")]] - #else - #define MAT_JSON_DLL - #endif - #endif + #if defined(_WIN32) && !defined(__CYGWIN__) + #ifdef MAT_JSON_EXPORTING + #define MAT_JSON_DLL __declspec(dllexport) + #else + #define MAT_JSON_DLL __declspec(dllimport) + #endif + #else + #ifdef MAT_JSON_EXPORTING + #define MAT_JSON_DLL [[gnu::visibility("default")]] + #else + #define MAT_JSON_DLL + #endif + #endif #else - #define MAT_JSON_DLL + #define MAT_JSON_DLL #endif namespace matjson { - enum class Type { - Object, - Array, - String, - Number, - Bool, - Null, - }; - - class ValueImpl; - - class Value; - - using Array = std::vector; - - class Object; - class ObjectImpl; - - using JsonException = std::runtime_error; - - // Specialize this class and implement the following methods (not all required) - // static T from_json(const matjson::Value&); - // static matjson::Value to_json(const T&); - // static bool is_json(const matjson::Value&); - template - struct Serialize; - - static constexpr int NO_INDENTATION = 0; - static constexpr int TAB_INDENTATION = -1; - - class MAT_JSON_DLL Value final { - std::unique_ptr m_impl; - friend ValueImpl; - Value(std::unique_ptr); - public: - Value(); - Value(std::string value); - Value(const char* value); - Value(double value); - Value(bool value); - Value(Object value); - Value(Array value); - Value(std::nullptr_t); - template - requires std::is_integral_v - Value(T value) : Value(static_cast(value)) {} - - Value(const Value&); - Value(Value&&); - ~Value(); - - Value& operator=(Value); - - template - requires requires(T value) { Serialize>::to_json(value); } - Value(T&& value) : Value(Serialize>::to_json(std::forward(value))) {} - - template - // Prevents implicit conversion from pointer to bool - Value(T*) = delete; - - static Value from_str(std::string_view source); - static std::optional from_str(std::string_view source, std::string& error) noexcept; - - std::optional> try_get(std::string_view key); - std::optional> try_get(std::string_view key) const; - std::optional> try_get(size_t index); - std::optional> try_get(size_t index) const; - - Value& operator[](std::string_view key); - const Value& operator[](std::string_view key) const; - Value& operator[](size_t index); - const Value& operator[](size_t index) const; - - void set(std::string_view key, Value value); - void erase(std::string_view key); - - bool try_set(std::string_view key, Value value) noexcept; - bool try_erase(std::string_view key) noexcept; - - Type type() const; - - bool as_bool() const; - std::string as_string() const; - int as_int() const; - double as_double() const; - - const Object& as_object() const&; - Object& as_object() &; - Object as_object() &&; - - const Array& as_array() const&; - Array& as_array() &; - Array as_array() &&; - - bool operator==(const Value&) const; - bool operator<(const Value&) const; - bool operator>(const Value&) const; - - bool is_null() const { return type() == Type::Null; } - bool is_string() const { return type() == Type::String; } - bool is_number() const { return type() == Type::Number; } - bool is_bool() const { return type() == Type::Bool; } - bool is_array() const { return type() == Type::Array; } - bool is_object() const { return type() == Type::Object; } - - bool contains(std::string_view key) const; - size_t count(std::string_view key) const; - - // Use matjson::NO_INDENTATION for a compact json, matjson::TAB_INDENTATION for tabs, - // otherwise specifies the amount of spaces - std::string dump(int indentation_size = 4) const; - - template - decltype(auto) as() const& { - if constexpr (std::is_same_v) { - return as_bool(); - } else if constexpr (std::is_integral_v) { - return as_int(); - } else if constexpr (std::is_floating_point_v) { - return as_double(); - } else if constexpr (requires(const Value& json) { Serialize>::from_json(json); }) { - return Serialize>::from_json(*this); - } else if constexpr (std::is_same_v) { - return as_array(); - } else if constexpr (std::is_same_v) { - return as_object(); - } else if constexpr (std::is_constructible_v) { - return as_string(); - } else if constexpr (std::is_same_v) { - return *this; - } else { - static_assert(!std::is_same_v, "no conversion found from matjson::Value to T"); - } - } - - template - decltype(auto) as() & { - if constexpr (std::is_same_v) { - return as_array(); - } else if constexpr (std::is_same_v) { - return as_object(); - } else { - return static_cast(this)->as(); - } - } - - template - decltype(auto) as() &&; - - template - bool is() const { - if constexpr (requires(const Value& json) { Serialize>::is_json(json); }) { - return Serialize>::is_json(*this); - } - if constexpr (std::is_same_v) { - return true; - } - switch (type()) { - case Type::Array: return std::is_same_v; - case Type::Object: return std::is_same_v; - case Type::String: return std::is_constructible_v; - case Type::Number: return std::is_integral_v || std::is_floating_point_v; - case Type::Bool: return std::is_same_v; - case Type::Null: return std::is_same_v; - } - return false; - } - - template - decltype(auto) get(Key&& key_or_index) const { - return this->operator[](std::forward(key_or_index)).template as(); - } - - template - decltype(auto) get(Key&& key_or_index) { - return this->operator[](std::forward(key_or_index)).template as(); - } - - template - std::optional try_get(Key&& key_or_index) const { - auto value = try_get(std::forward(key_or_index)); - if (value && value->get().template is()) { - return std::optional(value->get().template as()); - } - return std::nullopt; - } - - template - std::optional try_get(Key&& key_or_index) { - auto value = try_get(std::forward(key_or_index)); - if (value && value->get().template is()) { - return std::optional(value->get().template as()); - } - return std::nullopt; - } - }; - - class MAT_JSON_DLL Object final { - friend ObjectImpl; - using value_type = std::pair; - // TODO: maybe dont use std::vector's iterator - using iterator = typename std::vector::iterator; - using const_iterator = typename std::vector::const_iterator; - std::unique_ptr m_impl; - public: - Object(); - Object(const Object&); - Object(Object&&); - Object(std::initializer_list init); - ~Object(); - - Object& operator=(Object); - - size_t size() const; - bool empty() const; - - Value& operator[](std::string_view key); - - iterator begin(); - iterator end(); - - const_iterator begin() const; - const_iterator end() const; - - const_iterator cbegin() const; - const_iterator cend() const; - - iterator find(std::string_view key); - const_iterator find(std::string_view key) const; - - std::pair insert(const value_type& value); - iterator erase(const_iterator it); - size_t erase(std::string_view key); - void clear(); - - size_t count(std::string_view key) const; - bool contains(std::string_view key) const; - - bool operator==(const Object& other) const; - bool operator<(const Object&) const; - bool operator>(const Object&) const; - }; - - template - decltype(auto) Value::as() && { - if constexpr (std::is_same_v) { - return std::move(*this).as_array(); - } else if constexpr (std::is_same_v) { - return std::move(*this).as_object(); - } else { - return static_cast(this)->as(); - } - } - - inline Value parse(std::string_view source) { - return Value::from_str(source); - } - - inline std::optional parse(std::string_view source, std::string& error) noexcept { - return Value::from_str(source, error); - } + + enum class Type { + Object, + Array, + String, + Number, + Bool, + Null, + }; + + class ValueImpl; + + class Value; + + struct ParseError { + std::string message; + int offset = 0, line = 0, column = 0; + + inline ParseError(std::string msg, int offset, int line, int column) : + message(std::move(msg)), offset(offset), line(line), column(column) {} + + /// Returns a string representation of the error, useful for coercing into Result + /// methods, where the error type is a string. *Do not* rely on the format of this string, + /// as it may change in the future. Instead, just access the fields directly. + inline operator std::string() const { + if (line) { + return this->message + " at line " + std::to_string(this->line) + ", column " + + std::to_string(this->column); + } + return this->message; + } + }; + + static constexpr int NO_INDENTATION = 0; + static constexpr int TAB_INDENTATION = -1; + + /// Specialize this class and implement the following methods (not all required) + /// static Result fromJson(matjson::Value const&); + /// static matjson::Value toJson(T const&); + template + struct Serialize; + + template + concept CanSerialize = requires(matjson::Value const& value, T t) { + { Serialize>::toJson(t) } -> std::same_as; + }; + template + concept CanDeserialize = requires(matjson::Value const& value, T t) { + { Serialize>::fromJson(value) }; + }; + template + concept CanSerde = CanSerialize && CanDeserialize; + + /// Creates a JSON object from a list of key-value pairs + /// Example: + /// > auto obj = makeObject({ {"key", 123}, {"key2", "value"} }); + /// @param entries List of key-value pairs + /// @return The JSON object + Value makeObject(std::initializer_list>); + + class MAT_JSON_DLL Value { + std::unique_ptr m_impl; + friend ValueImpl; + Value(std::unique_ptr); + + friend Value matjson::makeObject(std::initializer_list>); + void setKey_(std::string_view key); + Value(std::vector, bool); + + public: + /// Defaults to a JSON object, for convenience + Value(); + Value(std::string value); + Value(std::string_view value); + Value(char const* value); + Value(std::vector value); + Value(std::nullptr_t); + Value(double value); + Value(bool value); + explicit Value(std::intmax_t value); + explicit Value(std::uintmax_t value); + + template + requires std::is_integral_v && std::is_signed_v + Value(T value) : Value(static_cast(value)) {} + + template + requires std::is_integral_v && std::is_unsigned_v + Value(T value) : Value(static_cast(value)) {} + + template + Value(T&& value) : + Value(Serialize>::toJson(std::forward(value))) {} + + template + // Prevents implicit conversion from pointer to bool + Value(T*) = delete; + + Value(Value const&); + Value(Value&&); + ~Value(); + + Value& operator=(Value); + + bool operator==(Value const&) const; + bool operator<(Value const&) const; + bool operator>(Value const&) const; + + /// Create an empty JSON object + static Value object(); + /// Create an empty JSON array + static Value array(); + + /// Parses JSON from a string + /// @param source The JSON string to parse + /// @return The parsed JSON value or an error + static geode::Result parse(std::string_view source); + + /// Parses JSON from an input stream + /// @param source Stream to parse + /// @return The parsed JSON value or an error + static geode::Result parse(std::istream& source); + + /// Dumps the JSON value to a string, with a given indentation. + /// If the given indentation is matjson::NO_INDENTATION, the json is compacted. + /// If the given indentation is matjson::TAB_INDENTATION, the json is indented with tabs. + /// @param indentationSize The number of spaces to use for indentation + /// @return The JSON string + /// @note Due to limitations in the JSON format, NaN and Infinity float values get converted to null. + /// This behavior is the same as many other JSON libraries. + std::string dump(int indentationSize = 4) const; + + /// Returns the value associated with the given key + /// @param key Object key + /// @return The value associated with the key, or an error if it does not exist. + geode::Result get(std::string_view key); + + /// Returns the value associated with the given key + /// @param key Object key + /// @return The value associated with the key, or an error if it does not exist. + geode::Result get(std::string_view key) const; + + /// Returns the value associated with the given index + /// @param index Array index + /// @return The value associated with the index, or an error if the index is out of bounds. + geode::Result get(std::size_t index); + + /// Returns the value associated with the given index + /// @param index Array index + /// @return The value associated with the index, or an error if the index is out of bounds. + geode::Result get(std::size_t index) const; + + /// Returns the value associated with the given key + /// @param key Object key + /// @return The value associated with the key + /// @note If the key does not exist, it is inserted into the object. + /// And if this is not an object, returns a null value + Value& operator[](std::string_view key); + + /// Returns the value associated with the given key + /// @param key Object key + /// @return The value associated with the key + /// @note If the key does not exist, or this is not an object, + /// returns a null value + Value const& operator[](std::string_view key) const; + + /// Returns the value associated with the given index + /// @param index Array index + /// @return The value associated with the index + /// @note If the index is out of bounds, or this is not an array, + /// returns a null value + Value& operator[](std::size_t index); + + /// Returns the value associated with the given index + /// @param index Array index + /// @return The value associated with the index + /// @note If the index is out of bounds, or this is not an array, + /// returns a null value + Value const& operator[](std::size_t index) const; + + /// Sets the value associated with the given key + /// @param key Object key + /// @param value The value to set + /// @note If this is not an object, nothing happens + void set(std::string_view key, Value value); + + /// Adds a value to the end of the array + /// @param value Value + /// @note If this is not an array, nothing happens + void push(Value value); + + /// Clears the array/object, removing all entries + /// @note If this is not an array or object, nothing happens + void clear(); + + /// Removes the value associated with the given key + /// @param key Object key + /// @return true if the key was removed, false otherwise + /// @note If this is not an object, nothing happens and returns false + bool erase(std::string_view key); + + /// Checks if the key exists in the object + /// @param key Object key + /// @return true if the key exists, false otherwise + /// @note If this is not an object, returns false + bool contains(std::string_view key) const; + + /// Returns the number of entries in the array/object. + /// @return The number of entries + /// @note If this is not an array or object, returns 0 + std::size_t size() const; + + /// Returns the type of the JSON value + Type type() const; + + /// Returns the key of the object entry, if it is one. + /// If this is not an entry in an object, returns an empty optional. + /// @return The key of the object entry + std::optional getKey() const; + + std::vector::iterator begin(); + std::vector::iterator end(); + + std::vector::const_iterator begin() const; + std::vector::const_iterator end() const; + + bool isNull() const { + return this->type() == Type::Null; + } + + bool isString() const { + return this->type() == Type::String; + } + + bool isNumber() const { + return this->type() == Type::Number; + } + + bool isBool() const { + return this->type() == Type::Bool; + } + + bool isArray() const { + return this->type() == Type::Array; + } + + bool isObject() const { + return this->type() == Type::Object; + } + + /// Returns the number as a boolean, if this is a boolean. + /// If this is not a boolean, returns an error. + geode::Result asBool() const; + + /// Returns the number as a string, if this is a string. + /// If this is not a string, returns an error. + geode::Result asString() const; + + /// Returns the number as a signed integer, if this is a number. + /// If this is not a number, returns an error. + geode::Result asInt() const; + + /// Returns the number as an unsigned integer, if this is a number. + /// If this is not a number, returns an error. + geode::Result asUInt() const; + + /// Returns the number as a double, if this is a number. + /// If this is not a number, returns an error. + geode::Result asDouble() const; + + /// Returns a reference to the array, if this is an array. + /// If this is not an array, returns an error. + geode::Result&> asArray() &; + + /// Returns the array, if this is an array. + /// If this is not an array, returns an error. + geode::Result> asArray() &&; + + /// Returns a reference to the array, if this is an array. + /// If this is not an array, returns an error. + geode::Result const&> asArray() const&; + + /// Converts the JSON value to a given type, possibly serializing to + /// a custom type via the Serialize specialization + /// @tparam T The type to convert to + /// @return The converted value or an error + template + decltype(auto) as() const { + if constexpr (std::is_same_v) { + return this->asBool(); + } + else if constexpr (std::is_integral_v) { + if constexpr (std::is_signed_v) { + return this->asInt().map([](std::intmax_t v) -> T { + return static_cast(v); + }); + } + else { + return this->asUInt().map([](std::uintmax_t v) -> T { + return static_cast(v); + }); + } + } + else if constexpr (std::is_floating_point_v) { + return this->asDouble().map([](double v) -> T { + return static_cast(v); + }); + } + else if constexpr (CanDeserialize) { + return Serialize>::fromJson(*this); + } + else if constexpr (std::is_constructible_v) { + return this->asString(); + } + else if constexpr (std::is_same_v, Value>) { + return geode::Result(geode::Ok(*this)); + } + else if constexpr (std::is_same_v, std::vector>) { + return this->asArray(); + } + else { + static_assert(!std::is_same_v, "no conversion found from matjson::Value to T"); + } + } + }; + + /// Parses JSON from a string + /// @param source The JSON string to parse + /// @return The parsed JSON value or an error + /// @note Shorthand for Value::parse + inline geode::Result parse(std::string_view source) { + return Value::parse(source); + } + + /// Parses JSON from an input stream + /// @param source Stream to parse + /// @return The parsed JSON value or an error + /// @note Shorthand for Value::parse + inline geode::Result parse(std::istream& stream) { + return Value::parse(stream); + } + + // This is used internally by C++ when destructuring the value, useful for range for loops: + // > for (auto const& [key, value] : object) { ... } + template + requires requires { std::is_same_v, Value>; } + decltype(auto) get(T&& value) { + if constexpr (Index == 0) { + auto const opt = value.getKey(); + if (!opt) return std::string(); + return std::string(*opt); + } + else if constexpr (Index == 1) { + return std::forward(value); + } + } + + inline Value makeObject(std::initializer_list> entries) { + std::vector arr; + for (auto const& [key, value] : entries) { + arr.emplace_back(value).setKey_(key); + } + return Value(std::move(arr), true); + } + + // For fmtlib, lol + inline std::string format_as(matjson::Value const& value) { + return value.dump(matjson::NO_INDENTATION); + } } +// allow destructuring +template <> +struct std::tuple_size : std::integral_constant {}; + +template <> +struct std::tuple_element<0, matjson::Value> { + using type = std::string; +}; + template <> -struct std::hash { - MAT_JSON_DLL std::size_t operator()(matjson::Value const& value) const; +struct std::tuple_element<1, matjson::Value> { + using type = matjson::Value; }; diff --git a/include/matjson/external/reflect b/include/matjson/external/reflect new file mode 100644 index 0000000..ccd9f35 --- /dev/null +++ b/include/matjson/external/reflect @@ -0,0 +1,1806 @@ +// +[Overview](#Overview) / [Examples](#Examples) / [API](#API) / [FAQ](#FAQ) + +## REFLECT: C++20 Static Reflection library + +[![MIT Licence](http://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/license/mit) +[![Version](https://img.shields.io/github/v/release/qlibs/reflect)](https://github.com/qlibs/reflect/releases) +[![Build](https://img.shields.io/badge/build-green.svg)](https://godbolt.org/z/zvooxGPP9) +[![Try it online](https://img.shields.io/badge/try%20it-online-blue.svg)](https://godbolt.org/z/oYhh1hfeo) + + > https://en.wikipedia.org/wiki/Reflective_programming + +### Features + +- Single header (https://raw.githubusercontent.com/qlibs/reflect/main/reflect - for integration see [FAQ](#faq)) +- Minimal [API](#api) +- Verifies itself upon include (can be disabled with `-DNTEST` - see [FAQ](#faq)) + - Basically guarantees no UB, no memory leaks* +- Compiles cleanly with ([`-fno-exceptions -fno-rtti -Wall -Wextra -Werror -pedantic -pedantic-errors | /W4 /WX`](https://godbolt.org/z/M747ocGfx)) +- Agnostic to compiler changes (no ifdefs for the compiler specific implementations - see [FAQ](#faq)) +- Optimized run-time execution and binary size (see [performance](#perf)) +- Fast compilation times (see [compilation times](#comp)) + +### Requirements + +- C++20 ([gcc-12+, clang-15+, msvc-19.36+](https://godbolt.org/z/xPc19Moef)) + +--- + +### Overview + +> Hello world (https://godbolt.org/z/oYhh1hfeo) + +```cpp +#include + +enum E { A, B }; +struct foo { int a; E b; }; + +constexpr auto f = foo{.a = 42, .b = B}; + +// reflect::size +static_assert(2 == reflect::size(f)); + +// reflect::type_id +static_assert(reflect::type_id(f.a) != reflect::type_id(f.b)); + +// reflect::type_name +static_assert("foo"sv == reflect::type_name(f)); +static_assert("int"sv == reflect::type_name(f.a)); +static_assert("E"sv == reflect::type_name(f.b)); + +// reflect::enum_name +static_assert("B"sv == reflect::enum_name(f.b)); + +// reflect::member_name +static_assert("a"sv == reflect::member_name<0>(f)); +static_assert("b"sv == reflect::member_name<1>(f)); + +// reflect::get +static_assert(42 == reflect::get<0>(f)); // by index +static_assert(B == reflect::get<1>(f)); + +static_assert(42 == reflect::get<"a">(f)); // by name +static_assert(B == reflect::get<"b">(f)); + +// reflect::to +constexpr auto t = reflect::to(f); +static_assert(42 == std::get<0>(t)); +static_assert(B == std::get<1>(t)); + +int main() { + reflect::for_each([](auto I) { + std::print("{}.{}:{}={} ({}/{}/{})\n", + reflect::type_name(f), // foo, foo + reflect::member_name(f), // a , b + reflect::type_name(reflect::get(f)), // int, E + reflect::get(f), // 42 , B + reflect::size_of(f), // 4 , 4 + reflect::align_of(f), // 4 , 4 + reflect::offset_of(f)); // 0 , 4 + }, f); +} + +// and more (see API)... +``` + +--- + +### Examples + +- [feature] Opt-in mixins - https://godbolt.org/z/sj7fYKoc3 +- [feature] Meta-programming (https://github.com/qlibs/mp) - https://godbolt.org/z/ds3KMGhqP +- [future] Structured Bindings can introduce a Pack (https://wg21.link/P1061) - https://godbolt.org/z/Ga3bc3KKW +- [performance] Minimal Perfect Hashing based `enum_name` (https://github.com/qlibs/mph) - https://godbolt.org/z/WM155vTfv + +--- + + +### Performance + +> Binary size (https://godbolt.org/z/7TbobjWfj) + +```cpp +struct foo { int bar; }; +auto type_name(const foo& f) { return reflect::type_name(f); } +``` + +```asm +type_name(foo const&): // $CXX -O3 -DNDEBUG + lea rdx, [rip + type_name] + mov eax, 3 + ret + +type_name + .ascii "foo" +``` + +```cpp +struct foo { int bar; }; +auto member_name(const foo& f) { return reflect::member_name<0>(f); } +``` + +```asm +member_name(foo const&): // $CXX -O3 -DNDEBUG + lea rdx, [rip + member_name<0ul, foo>] + mov eax, 3 + ret + +member_name<0ul, foo> + .ascii "bar" +``` + +```cpp +enum class E { A, B, }; +auto enum_name(const E e) { return reflect::enum_name(e); } +``` + +```asm +enum_name(E): // $CXX -O3 -DNDEBUG (generates switch) + xor eax, eax + xor ecx, ecx + cmp edi, 1 + sete cl + lea rdx, [rip + enum_name<0>] + cmove rax, rdx + test edi, edi + lea rdx, [rip + enum_name<1>] + cmovne rdx, rax + mov eax, 1 + cmovne rax, rcx + ret + +enum_name<0ul>: + .ascii "A" + +enum_name<1ul>: + .ascii "B" +``` + + +### Compilation times + +> [include] https://raw.githubusercontent.com/qlibs/reflect/main/reflect + +```cpp +time g++-13.2 -x c++ -std=c++20 reflect -c -DNTEST # 0.113s +time g++-13.2 -x c++ -std=c++20 reflect -c # 0.253s +``` + +```cpp +time clang++-17 -x c++ -std=c++20 reflect -c -DNTEST # 0.119s +time clang++-17 -x c++ -std=c++20 reflect -c # 0.322s +``` + +--- + +### API + +```cpp +template requires std::is_aggregate_v> +[[nodiscard]] constexpr auto visit(Fn&& fn, T&& t) noexcept; +``` + +```cpp +struct foo { int a; int b; }; +static_assert(2 == visit([](auto&&... args) { return sizeof...(args); }, foo{})); +``` + +```cpp +template requires std::is_aggregate_v +[[nodiscard]] constexpr auto size() -> std::size_t; + +template requires std::is_aggregate_v +[[nodiscard]] constexpr auto size(const T&) -> std::size_t; +``` + +```cpp +struct foo { int a; int b; } f; +static_assert(2 == size()); +static_assert(2 == size(f)); +``` + +```cpp +template [[nodiscard]] constexpr auto type_name() noexcept; +template [[nodiscard]] constexpr auto type_name(const T&) noexcept; +``` + +```cpp +struct foo { int a; int b; }; +static_assert(std::string_view{"foo"} == type_name()); +static_assert(std::string_view{"foo"} == type_name(foo{})); +``` + +```cpp +template [[nodiscard]] constexpr auto type_id() noexcept; +template [[nodiscard]] constexpr auto type_id(T&&) noexcept; +``` + +```cpp +struct foo { }; +struct bar { }; +static_assert(type_id(foo{}) == type_id(foo{})); +static_assert(type_id(bar{}) != type_id()); +``` + +```cpp +template +[[nodiscard]] constexpr auto to_underlying(const E e) noexcept; + +template requires std::is_enum_v +consteval auto enum_min(const E = {}) { return REFLECT_ENUM_MIN; } + +template requires std::is_enum_v +consteval auto enum_max(const E = {}) { return REFLECT_ENUM_MAX; } + +template + requires (std::is_enum_v and Max > Min) +[[nodiscard]] constexpr auto enum_name(const E e) noexcept -> std::string_view { +``` + +```cpp +enum class Enum { foo = 1, bar = 2 }; +static_assert(std::string_view{"foo"} == enum_name(Enum::foo)); +static_assert(std::string_view{"bar"} == enum_name(Enum::bar)); +``` + +```cpp +enum class Enum { foo = 1, bar = 1024 }; +consteval auto enum_min(Enum) { return Enum::foo; } +consteval auto enum_max(Enum) { return Enum::bar; } + +static_assert(std::string_view{"foo"} == enum_name(Enum::foo)); +static_assert(std::string_view{"bar"} == enum_name(Enum::bar)); +``` + +```cpp +template + requires (std::is_aggregate_v and N < size()) +[[nodiscard]] constexpr auto member_name(const T& = {}) noexcept; +``` + +```cpp +struct foo { int a; int b; }; +static_assert(std::string_view{"a"} == member_name<0, foo>()); +static_assert(std::string_view{"a"} == member_name<0>(foo{})); +static_assert(std::string_view{"b"} == member_name<1, foo>()); +static_assert(std::string_view{"b"} == member_name<1>(foo{})); +``` + +```cpp +template + requires (std::is_aggregate_v> and + N < size>()) +[[nodiscard]] constexpr decltype(auto) get(T&& t) noexcept; +``` + +```cpp +struct foo { int a; bool b; }; +constexpr auto f = foo{.i=42, .b=true}; +static_assert(42 == get<0>(f)); +static_assert(true == get<1>(f)); +``` + +```cpp +template requires std::is_aggregate_v +concept has_member_name = /*unspecified*/ +``` + +```cpp +struct foo { int a; int b; }; +static_assert(has_member_name); +static_assert(has_member_name); +static_assert(not has_member_name); +``` + +```cpp +template requires has_member_name +constexpr decltype(auto) get(T&& t) noexcept; +``` + +```cpp +struct foo { int a; int b; }; +constexpr auto f = foo{.i=42, .b=true}; +static_assert(42 == get<"a">(f)); +static_assert(true == get<"b">(f)); +``` + +```cpp +template + requires (std::is_aggregate_v and std::is_aggregate_v) +constexpr auto copy(const TSrc& src, TDst& dst) noexcept -> void; +``` + +```cpp +struct foo { int a; int b; }; +struct bar { int a{}; int b{}; }; + +bar b{}; +foo f{}; + +copy(f, b); +assert(b.a == f.a); +assert(b.b == f.b); + +copy<"a">(f, b); +assert(b.a == f.a); +assert(0 == b.b); +``` + +```cpp +template class R, class T> + requires std::is_aggregate_v> +[[nodiscard]] constexpr auto to(T&& t) noexcept; +``` + +```cpp +struct foo { int a; int b; }; + +constexpr auto t = to(foo{.a=4, .b=2}); +static_assert(4 == std::get<0>(t)); +static_assert(2 == std::get<1>(t)); + +auto f = foo{.a=4, .b=2}; +auto t = to(f); +std::get<0>(t) *= 10; +f.b = 42; +assert(40 == std::get<0>(t) and 40 == f.a); +assert(42 == std::get<1>(t) and 42 == f.b); +``` + +```cpp +template +[[nodiscard]] constexpr auto to(T&& t); +``` + +```cpp +struct foo { int a; int b; }; +struct baz { int a{}; int c{}; }; + +const auto b = to(foo{.a=4, .b=2}); +assert(4 == b.a and 0 == b.c); +``` + +```cpp +template requires std::is_aggregate_v +[[nodiscard]] constexpr auto size_of() -> std::size_t; + +template requires std::is_aggregate_v +[[nodiscard]] constexpr auto size_of(T&&) -> std::size_t; + +template requires std::is_aggregate_v +[[nodiscard]] constexpr auto align_of() -> std::size_t; + +template requires std::is_aggregate_v +[[nodiscard]] constexpr auto align_of(T&&) -> std::size_t; + +template requires std::is_aggregate_v +[[nodiscard]] constexpr auto offset_of() -> std::size_t; + +template requires std::is_aggregate_v +[[nodiscard]] constexpr auto offset_of(T&&) -> std::size_t; +``` + +```cpp +struct foo { int a; bool b; }; + +static_assert(4 == size_of<0, foo>()); +static_assert(1 == size_of<1, foo>()); +static_assert(4 == align_of<0, foo>()); +static_assert(1 == align_of<1, foo>()); +static_assert(0 == offset_of<0, foo>()); +static_assert(4 == offset_of<1, foo>()); +``` + +```cpp +template + requires std::is_aggregate_v> +constexpr auto for_each(Fn&& fn) -> void; + +template + requires std::is_aggregate_v> +constexpr auto for_each(Fn&& fn, T&& t) -> void; +``` + +```cpp +struct { int a; int b; } f; + +reflect::for_each([&f](const auto I) { + std::print("{}:{}={}", member_name(f), get(f)); // prints a:int=4, b:int=2 +}, f); +``` + +> Configuration + +```cpp +#define REFLECT_ENUM_MIN 0 // Min size for enum name (can be overridden) + // For example: `-DREFLECT_ENUM_MIN=-1` +#define REFLECT_ENUM_MAX 128 // Max size for enum name (can be overridden) + // For example: `-DREFLECT_ENUM_MAX=32` +``` + +--- + +### FAQ + +- How does `reflect` compare to https://wg21.link/P2996? + + > `reflect` library only provides basic reflection primitives, mostly via hacks and workarounds to deal with lack of the reflection. + https://wg21.link/P2996 is a language proposal with many more features and capabilities. + +- How does `reflect` work under the hood? + + > There are many different ways to implement reflection. `reflect` uses C++20's structure bindings, concepts and source_location to do it. See `visit` implementation for more details. + +- How can `reflect` be agnostic to compiler changes? + + > `reflect` precomputes required prefixes/postfixes to find required names from the `source_location::function_name()` output for each compiler upon inclusion. + Any compiler change will end up with new prefixes/postfixes and wont require additional maintenance. + +- What does it mean that `reflect` tests itself upon include? + + > `reflect` runs all tests (via static_asserts) upon include. If the include compiles it means all tests are passing and the library works correctly on given compiler, environment. + +- What is compile-time overhead of `reflect` library? + + > `reflect` include takes ~.2s (that includes running all tests). + The most expensive calls are `visit` and `enum_to_name` whose timing will depend on the number of reflected elements and/or min/max values provided. + There are no recursive template instantiations in the library. + +- Can I disable running tests at compile-time for faster compilation times? + + > When `-DNTEST` is defined static_asserts tests wont be executed upon inclusion. + Note: Use with caution as disabling tests means that there are no guarantees upon inclusion that the given compiler/env combination works as expected. + +- How to extend the number of members to be reflected (default: 64)? + + > Override `visit`, for example - https://godbolt.org/z/Ga3bc3KKW + + ```cpp + template // requires https://wg21.link/P1061 + [[nodiscard]] constexpr decltype(auto) visit(Fn&& fn, T&& t) noexcept { + auto&& [... ts] = std::forward(t); + return std::forward(fn)(std::forward_like(ts)...); + } + ``` + +- How to integrate with [CMake.FetchContent](https://cmake.org/cmake/help/latest/module/FetchContent.html)? + + ``` + include(FetchContent) + + FetchContent_Declare( + qlibs.reflect + GIT_REPOSITORY https://github.com/qlibs/reflect + GIT_TAG v1.2.4 + ) + + FetchContent_MakeAvailable(qlibs.reflect) + ``` + + ``` + target_link_libraries(${PROJECT_NAME} PUBLIC qlibs.reflect); + ``` +- Similar projects? + > [boost.pfr](https://github.com/boostorg/pfr), [glaze](https://github.com/stephenberry/glaze), [reflect-cpp](https://github.com/getml/reflect-cpp), [magic_enum](https://github.com/Neargye/magic_enum) +