From 8773f9fa7cda2f17a5e226e9eff93b3ef96ba544 Mon Sep 17 00:00:00 2001 From: jvallve Date: Fri, 16 Oct 2026 21:22:27 +0200 Subject: [PATCH] follow cycles now raises error, removed unused parameter in checkSquemaValue/Default/Options, type_check.hpp macros moved to .cpp, added doxygen documentation to all library --- CMakeLists.txt | 21 ++ README.md | 17 +- doc/Doxyfile.in | 35 +++ include/yaml-schema-cpp/expression.hpp | 48 ++- .../yaml-schema-cpp/filesystem_wrapper.hpp | 6 +- include/yaml-schema-cpp/type_check.hpp | 139 ++++----- include/yaml-schema-cpp/yaml_conversion.hpp | 56 +++- include/yaml-schema-cpp/yaml_generator.hpp | 54 +++- include/yaml-schema-cpp/yaml_schema.hpp | 280 ++++++++++++++++-- include/yaml-schema-cpp/yaml_server.hpp | 115 ++++++- include/yaml-schema-cpp/yaml_utils.hpp | 218 ++++++++++---- src/expression.cpp | 94 ++++-- src/type_check.cpp | 149 ++++++++-- src/yaml_generator.cpp | 112 +++++-- src/yaml_schema.cpp | 231 ++++++++------- src/yaml_server.cpp | 6 + src/yaml_template_generator.cpp | 37 ++- src/yaml_utils.cpp | 231 ++++++++++++--- test/gtest_apply_schema.cpp | 178 ++++++++++- test/gtest_check_type.cpp | 58 ++++ test/gtest_expression.cpp | 69 +++++ test/gtest_flatten.cpp | 29 +- test/gtest_generator.cpp | 62 ++++ test/gtest_relative_path.cpp | 65 +++- test/gtest_schema.cpp | 4 +- test/gtest_yaml_utils.cpp | 73 +++++ .../schema/regression/regression_array.schema | 4 + .../regression_derived_options.schema | 9 + .../regression_derived_value.schema | 9 + .../regression_expression_eval_fail.schema | 8 + .../regression_expression_order.schema | 10 + .../regression_expression_string_type.schema | 9 + test/schema/regression/regression_map.schema | 6 + .../regression/regression_null_string.schema | 8 + .../regression/regression_scalar_leaf.schema | 7 + .../regression/regression_slash_key.schema | 17 ++ .../regression_template_quotes.schema | 17 ++ test/wrong_schema/follow_cycle.schema | 6 + test/wrong_schema/follow_cycle2.schema | 6 + test/wrong_schema/value_without_doc.schema | 7 + test/yaml/follow_cycle/a.yaml | 2 + test/yaml/follow_cycle/b.yaml | 2 + test/yaml/follow_diamond/common.yaml | 1 + test/yaml/follow_diamond/left.yaml | 2 + test/yaml/follow_diamond/main.yaml | 3 + test/yaml/follow_diamond/right.yaml | 2 + test/yaml/relative_follow/main.yaml | 4 + test/yaml/relative_follow/sub/sub.yaml | 3 + 48 files changed, 2105 insertions(+), 424 deletions(-) create mode 100644 doc/Doxyfile.in create mode 100644 test/schema/regression/regression_array.schema create mode 100644 test/schema/regression/regression_derived_options.schema create mode 100644 test/schema/regression/regression_derived_value.schema create mode 100644 test/schema/regression/regression_expression_eval_fail.schema create mode 100644 test/schema/regression/regression_expression_order.schema create mode 100644 test/schema/regression/regression_expression_string_type.schema create mode 100644 test/schema/regression/regression_map.schema create mode 100644 test/schema/regression/regression_null_string.schema create mode 100644 test/schema/regression/regression_scalar_leaf.schema create mode 100644 test/schema/regression/regression_slash_key.schema create mode 100644 test/schema/regression/regression_template_quotes.schema create mode 100644 test/wrong_schema/follow_cycle.schema create mode 100644 test/wrong_schema/follow_cycle2.schema create mode 100644 test/wrong_schema/value_without_doc.schema create mode 100644 test/yaml/follow_cycle/a.yaml create mode 100644 test/yaml/follow_cycle/b.yaml create mode 100644 test/yaml/follow_diamond/common.yaml create mode 100644 test/yaml/follow_diamond/left.yaml create mode 100644 test/yaml/follow_diamond/main.yaml create mode 100644 test/yaml/follow_diamond/right.yaml create mode 100644 test/yaml/relative_follow/main.yaml create mode 100644 test/yaml/relative_follow/sub/sub.yaml diff --git a/CMakeLists.txt b/CMakeLists.txt index f4d6fc3..17de0de 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,6 +45,7 @@ set(LIB_INSTALL_DIR lib) if(NOT BUILD_TESTS) option(BUILD_TESTS "Build Unit tests" ON) endif(NOT BUILD_TESTS) +option(BUILD_DOC "Build documentation (requires doxygen)" OFF) if(CMAKE_CXX_STANDARD LESS 17) message(STATUS "C++ less than 17, Boost::filesystem is required") set(BOOST_FILESYSTEM_LIB 1) @@ -136,6 +137,25 @@ endif() # find the installed library relative to the installed executable (works for any CMAKE_INSTALL_PREFIX) set_target_properties(yaml_template_generator PROPERTIES INSTALL_RPATH "$ORIGIN/../lib") +# ------ DOCUMENTATION ------ +# 'make doc' generates the documentation in ${PROJECT_BINARY_DIR}/doc/html (if doxygen is found). +# With BUILD_DOC=ON, it is also generated by 'make'. +find_package(Doxygen QUIET) +if(DOXYGEN_FOUND AND NOT TARGET doc) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/doc/Doxyfile.in ${PROJECT_BINARY_DIR}/Doxyfile @ONLY) + if(BUILD_DOC) + message(STATUS "Will build documentation.") + set(DOC_ALL ALL) + endif() + add_custom_target(doc ${DOC_ALL} + COMMAND Doxygen::doxygen ${PROJECT_BINARY_DIR}/Doxyfile + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + COMMENT "Generating documentation in ${PROJECT_BINARY_DIR}/doc/html" + VERBATIM) +elseif(BUILD_DOC AND NOT DOXYGEN_FOUND) + message(WARNING "BUILD_DOC is ON but doxygen was not found: the documentation will not be built.") +endif() + # ------ INSTALL ------ #install headers install( @@ -186,6 +206,7 @@ install(FILES # So that it can be used without installing export(EXPORT ${PROJECT_NAME}-targets FILE "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}-targets.cmake" + NAMESPACE yaml-schema-cpp:: ) # ---- UNINSTALL ---- diff --git a/README.md b/README.md index c25a67f..adc632e 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,17 @@ sudo make install ``` You can run the tests with `ctest -j4`. +## Documentation + +If [doxygen](https://www.doxygen.nl) is installed (`sudo apt install doxygen`), the API documentation can be generated +from the build folder with: + +```bash +make doc +``` + +and then opened at `build/doc/html/index.html`. Configure with `cmake -DBUILD_DOC=ON ..` to generate it also with `make`. + # Using `yaml_schema_cpp` ## The `.schema` file @@ -146,6 +157,8 @@ The `_type` can be: If the type string ends with `[]`, the input field is specified to be a sequence. +Note that null values (e.g. `key:` without value) are not accepted for string types. Use `''` for an empty string. + ### `_mandatory` A **bool** value or expression (see below) specifying if the input field is required or just optional. @@ -171,7 +184,7 @@ In both input and schema files, the key `follow` is interpreted as a copy-paste We allow the use of [exprtk](http://www.partow.net/programming/exprtk/index.html) expressions in `_mandatory` field. There are some rules for the expressions: 1. It should start by a `$` symbol. -2. All parameters involved in an expression have to be at the same level of the specified node. +2. All parameters involved in an expression have to be at the same level of the specified node (they can be declared before or after it). 3. All parameters involved in an expression have to be mandatory at all cases (i.e. true, not an expression) or have a default value defined. 4. The exprtk syntax has to be followed (see [exprtk readme](https://www.partow.net/programming/exprtk/code/readme.txt) for more information). 5. The result of the expression will be casted to `bool`. If the result is a numeric value, it will be converted to `true` except for 0 (`false`). @@ -283,7 +296,7 @@ This library handles relative paths parameters. In case a string value starts by This is useful in case of using `follow` to a YAML in a different folder that contains a path parameter. ### Tree structure -This library is intended to be used to validate a given YAML file with parameters. Then, loops are not suported in the input YAML file. It is assumed to have tree structure. Note that `follow` copies the content of the other file, so multiple `follow` instantes to the same file is supported. +This library is intended to be used to validate a given YAML file with parameters. Then, loops are not suported in the input YAML file. It is assumed to have tree structure. Note that `follow` copies the content of the other file, so multiple `follow` instantes to the same file is supported. A `follow` cycle (a file that follows, directly or indirectly, itself) raises an error. # YAML template generator diff --git a/doc/Doxyfile.in b/doc/Doxyfile.in new file mode 100644 index 0000000..1ff128d --- /dev/null +++ b/doc/Doxyfile.in @@ -0,0 +1,35 @@ +# Doxygen configuration of yaml-schema-cpp (configured by CMake, see 'doc' target in CMakeLists.txt). +# Only the tags different from the doxygen defaults are set. + +PROJECT_NAME = "yaml-schema-cpp" +PROJECT_NUMBER = @PROJECT_VERSION@ +PROJECT_BRIEF = "Specification and validation of YAML files via schemas" +OUTPUT_DIRECTORY = @PROJECT_BINARY_DIR@/doc +STRIP_FROM_PATH = @PROJECT_SOURCE_DIR@/include + +# Input: README (main page) and public headers (without the redistributed exprtk) +INPUT = @PROJECT_SOURCE_DIR@/README.md \ + @PROJECT_SOURCE_DIR@/include/yaml-schema-cpp +FILE_PATTERNS = *.hpp \ + *.md +RECURSIVE = YES +EXCLUDE = @PROJECT_SOURCE_DIR@/include/yaml-schema-cpp/exprtk +USE_MDFILE_AS_MAINPAGE = @PROJECT_SOURCE_DIR@/README.md + +# Extraction +EXTRACT_PRIVATE = YES +EXTRACT_STATIC = YES + +# Preprocessing: document the Eigen parts +ENABLE_PREPROCESSING = YES +PREDEFINED = _EIGEN_FOUND=1 + +# Warnings +QUIET = YES +WARN_IF_UNDOCUMENTED = YES +WARN_IF_DOC_ERROR = YES +WARN_NO_PARAMDOC = YES + +# Output +GENERATE_HTML = YES +GENERATE_LATEX = NO diff --git a/include/yaml-schema-cpp/expression.hpp b/include/yaml-schema-cpp/expression.hpp index 28be851..052e343 100644 --- a/include/yaml-schema-cpp/expression.hpp +++ b/include/yaml-schema-cpp/expression.hpp @@ -1,17 +1,63 @@ +/** + * @file expression.hpp + * @brief Boolean expressions in the MANDATORY key of schema specifications. + * + * Expressions start with '$' and follow the exprtk syntax (http://www.partow.net/programming/exprtk). The symbols are + * the names of other parameters at the same level in the schema/input (e.g. "$mode == 'auto' and n_threads > 0"). + */ #pragma once #include "yaml-cpp/yaml.h" namespace yaml_schema_cpp { +/** + * @brief Check if a node is an expression. + * @param node YAML node + * @return true if node is a scalar starting with '$' + */ bool isExpression(const YAML::Node& node); +/** + * @brief Check if a string is an expression. + * @param expression string + * @return true if expression starts with '$' + */ bool isExpression(const std::string& expression); +/** + * @brief Check the syntax of an expression in a schema. + * + * All symbols in the expression should be parameters specified in node_schema_parent (i.e. at the same level) of + * basic type (see isBasicType()) or string type, either mandatory (MANDATORY true) or with DEFAULT. + * + * @param node_expression node containing the expression (starting with '$') + * @param node_schema_parent schema node containing the specifications of the parameters in the expression + * @param err OUTPUT error message (if not valid) + * @return true if the expression is valid + */ bool checkExpression(const YAML::Node& node_expression, const YAML::Node& node_schema_parent, std::string& err); -bool evalExpression(std::string expression_str, const YAML::Node& node_input_parent); +/** + * @brief Evaluate an expression with the values of an input node. + * + * The values of the symbols are taken from node_input_parent. Their type (bool, number or string) is taken from the + * specifications in node_schema_parent if available, otherwise it is deduced from the value. + * + * @param expression_str expression (starting with '$') + * @param node_input_parent input node containing the parameters in the expression + * @param node_schema_parent schema node containing the specifications of the parameters (undefined if not available) + * @return result of the expression casted to bool (numeric values are true except for 0) + * @throws std::runtime_error if the expression cannot be compiled (e.g. wrong syntax or a parameter is missing) + */ +bool evalExpression(std::string expression_str, + const YAML::Node& node_input_parent, + const YAML::Node& node_schema_parent = YAML::Node(YAML::NodeType::Undefined)); +/** + * @brief Remove the leading '$' of an expression (if any). + * @param expression_str INPUT/OUTPUT expression + */ void preProcessExpression(std::string& expression_str); } // namespace yaml_schema_cpp diff --git a/include/yaml-schema-cpp/filesystem_wrapper.hpp b/include/yaml-schema-cpp/filesystem_wrapper.hpp index 43ea95a..9e13226 100644 --- a/include/yaml-schema-cpp/filesystem_wrapper.hpp +++ b/include/yaml-schema-cpp/filesystem_wrapper.hpp @@ -1,3 +1,7 @@ +/** + * @file filesystem_wrapper.hpp + * @brief Namespace alias 'filesystem': std::filesystem (C++17 or later) or boost::filesystem (older standards). + */ #pragma once #include "yaml-schema-cpp/internal/config.h" @@ -8,4 +12,4 @@ namespace filesystem = boost::filesystem; #else #include namespace filesystem = std::filesystem; -#endif \ No newline at end of file +#endif diff --git a/include/yaml-schema-cpp/type_check.hpp b/include/yaml-schema-cpp/type_check.hpp index bbefa26..10eb188 100644 --- a/include/yaml-schema-cpp/type_check.hpp +++ b/include/yaml-schema-cpp/type_check.hpp @@ -1,3 +1,12 @@ +/** + * @file type_check.hpp + * @brief Checking if YAML nodes can be interpreted as a given type (specified as a string). + * + * Types are specified as strings: basic types ("bool", "char", "int", "unsigned int", "long int", + * "long unsigned int", "float", "double"), string types ("string", "std::string"), Eigen types ("VectorXd", + * "MatrixXd", "Vectord", "Matrixd" for N in 1..10, with or without "Eigen::", only if Eigen was found) and array + * types ("[]" or "[]", e.g. "double[3][]"). + */ #pragma once #include "yaml-schema-cpp/internal/config.h" @@ -11,64 +20,6 @@ namespace yaml_schema_cpp { -#define CHECK_TYPE_CASE(TypeName) \ - if (type == #TypeName) \ - { \ - node.as(); \ - return true; \ - } - -#if _EIGEN_FOUND == 1 -#define CHECK_TYPE_EIGEN_CASE(size) \ - if (type == "Vector" #size "d" or type == "Eigen::Vector" #size "d") \ - { \ - node.as >(); \ - return true; \ - } \ - if (type == "Matrix" #size "d" or type == "Eigen::Matrix" #size "d") \ - { \ - node.as >(); \ - return true; \ - } - -#define CHECK_TYPE_EIGEN_CASES \ - CHECK_TYPE_EIGEN_CASE(1) \ - CHECK_TYPE_EIGEN_CASE(2) \ - CHECK_TYPE_EIGEN_CASE(3) \ - CHECK_TYPE_EIGEN_CASE(4) \ - CHECK_TYPE_EIGEN_CASE(5) \ - CHECK_TYPE_EIGEN_CASE(6) \ - CHECK_TYPE_EIGEN_CASE(7) \ - CHECK_TYPE_EIGEN_CASE(8) \ - CHECK_TYPE_EIGEN_CASE(9) \ - CHECK_TYPE_EIGEN_CASE(10) \ - CHECK_TYPE_CASE(Eigen::VectorXd) \ - CHECK_TYPE_CASE(Eigen::VectorXd) \ - CHECK_TYPE_CASE(Eigen::MatrixXd) \ - { \ - using namespace Eigen; \ - CHECK_TYPE_CASE(VectorXd) \ - CHECK_TYPE_CASE(MatrixXd) \ - } -#endif - -#define CHECK_TYPE_BASIC_CASES \ - CHECK_TYPE_CASE(bool) \ - CHECK_TYPE_CASE(char) \ - CHECK_TYPE_CASE(int) \ - CHECK_TYPE_CASE(unsigned int) \ - CHECK_TYPE_CASE(long int) \ - CHECK_TYPE_CASE(long unsigned int) \ - CHECK_TYPE_CASE(float) \ - CHECK_TYPE_CASE(double) - -#define CHECK_TYPE_STRING_CASES \ - CHECK_TYPE_CASE(std::string) \ - { \ - using std::string; \ - CHECK_TYPE_CASE(string) \ - } - /** * @brief check the sizes of node and type * If the type is an array type, it checks the node is a sequence. @@ -81,43 +32,95 @@ namespace yaml_schema_cpp bool checkSizes(const YAML::Node& node, const std::string& type); /** - * @brief check if a node can be interpreted as a given type - * NOTE: This function may raise exceptions + * @brief check if a node can be interpreted as a given basic type (or array of basic types) + * NOTE: This function may raise exceptions (if the type is basic but the conversion fails) * * @param node YAML node to be converted * @param type string defining the type - * @return false if type not checked, true if check ok. + * @return false if type is not basic (or sizes do not match), true if check ok. */ bool checkNodeAsBasic(const YAML::Node& node, const std::string& type); + +/** + * @brief check if a node can be interpreted as a given string type (or array of string types) + * NOTE: This function may raise exceptions (if the type is string but the conversion fails) + * + * @param node YAML node to be converted + * @param type string defining the type + * @return false if type is not string (or sizes do not match), true if check ok. + */ bool checkNodeAsString(const YAML::Node& node, const std::string& type); + #if _EIGEN_FOUND == 1 +/** + * @brief check if a node can be interpreted as a given Eigen type (or array of Eigen types) + * NOTE: This function may raise exceptions (if the type is Eigen but the conversion fails) + * + * @param node YAML node to be converted + * @param type string defining the type + * @return false if type is not Eigen (or sizes do not match), true if check ok. + */ bool checkNodeAsEigen(const YAML::Node& node, const std::string& type); #endif + +/** + * @brief check if a node can be interpreted as a given trivial type (or array of trivial types) + * NOTE: This function may raise exceptions (if the type is trivial but the conversion fails) + * + * @param node YAML node to be converted + * @param type string defining the type + * @return false if type is not trivial (or sizes do not match), true if check ok. + */ bool checkNodeAs(const YAML::Node& node, const std::string& type); /** * @brief try if a node can be interpreted as a given type * NOTE: This function does not raise exceptions + * NOTE: A null node can be interpreted as a string ("null"), see applySchema() for input validation. * * @param node YAML node to be converted - * @param type string defining the type + * @param type string defining the type (trivial type or array of trivial types) * @return if the node can be interpreted as the type */ bool tryNodeAs(const YAML::Node& node, const std::string& type); -/** Functions to evaluate if a string definig type is in these groups: - * - Basic: int, float, string, Eigen types (if eigen installed)) - * - String - * - Eigen: Vector or Matrix dynamic of fixed size (until 10) - * - Trivial: Basic, string or Eigen - * - NonTrivial: rest of types that require schemas to be specified +/** + * @brief check if a type is trivial: basic, string or Eigen (not requiring a schema) + * @param type string defining the type + * @return true if trivial (false for array types and malformed type strings) */ bool isTrivialType(const std::string& type); + +/** + * @brief check if a type is basic: bool, char, int, unsigned int, long int, long unsigned int, float or double + * @param type string defining the type + * @return true if basic (false for array types and malformed type strings) + */ bool isBasicType(const std::string& type); + +/** + * @brief check if a type is string: string or std::string + * @param type string defining the type + * @return true if string (false for array types and malformed type strings) + */ bool isStringType(const std::string& type); + #if _EIGEN_FOUND == 1 +/** + * @brief check if a type is Eigen: VectorXd, MatrixXd, Vectord or Matrixd (N from 1 to 10), with or without + * "Eigen::" + * @param type string defining the type + * @return true if Eigen (false for array types and malformed type strings) + */ bool isEigenType(const std::string& type); #endif + +/** + * @brief check if a type is non-trivial: a schema file with that name exists in folders (see findSchema()) + * @param type string defining the type (name of the schema) + * @param folders folders where the schema files are searched (recursively) + * @return true if the schema file is found + */ bool isNonTrivialType(const std::string& type, const std::vector& folders); } // namespace yaml_schema_cpp diff --git a/include/yaml-schema-cpp/yaml_conversion.hpp b/include/yaml-schema-cpp/yaml_conversion.hpp index 01e58b9..84fd862 100644 --- a/include/yaml-schema-cpp/yaml_conversion.hpp +++ b/include/yaml-schema-cpp/yaml_conversion.hpp @@ -1,3 +1,8 @@ +/** + * @file yaml_conversion.hpp + * @brief YAML conversions (YAML::convert) of Eigen types: Matrix, Quaternion and expressions (only if Eigen was + * found). + */ #pragma once #include "yaml-schema-cpp/internal/config.h" @@ -8,8 +13,8 @@ #include // Eigen -#include -#include +#include +#include // stl #include @@ -22,7 +27,9 @@ namespace YAML template struct convert > { - /** \brief Encode a YAML Sequence from an Eigen::Matrix< > + /** \brief Encode a YAML Sequence (row-major) from an Eigen::Matrix< > + * \param matrix matrix to be encoded + * \return the YAML sequence */ static Node encode(const Eigen::Matrix<_Scalar, _Rows, _Cols, _Options, _MaxRows, _MaxCols>& matrix) { @@ -52,6 +59,12 @@ struct convert() throws then) */ static bool decode(const Node& node, Eigen::Matrix<_Scalar, _Rows, _Cols, _Options, _MaxRows, _MaxCols>& matrix) { @@ -63,9 +76,10 @@ struct convert error if (_Rows == Eigen::Dynamic && _Cols == Eigen::Dynamic) { - std::cout << "Bad yaml format. A full dynamic matrix requires [ [rows, cols], [... values ...] ]" - << std::endl; return false; } values = node; @@ -136,8 +144,6 @@ struct convert struct convert > { + /** \brief Encode a YAML Sequence [x, y, z, w] from an Eigen::Quaternion + * \param quaternion quaternion to be encoded + * \return the YAML sequence + */ static Node encode(const Eigen::Quaternion<_Scalar, _Options>& quaternion) { Node node(NodeType::Sequence); @@ -199,12 +206,17 @@ struct convert > return node; } + /** \brief Decode a YAML sequence [x, y, z, w] into an Eigen::Quaternion (not normalized) + * + * \param node YAML node to be decoded + * \param quaternion OUTPUT decoded quaternion + * \return false if the node is not a sequence of 4 elements + */ static bool decode(const Node& node, Eigen::Quaternion<_Scalar, _Options>& quaternion) { int nSize = node.size(); // Sequence check is implicit if (nSize != 4) { - std::cout << "Wrong quaternion input size!" << std::endl; return false; } else @@ -226,6 +238,10 @@ struct convert > template struct convert > { + /** \brief Encode a YAML Sequence (row-major) from an Eigen::CwiseNullaryOp + * \param matrix expression to be encoded + * \return the YAML sequence + */ static Node encode(const Eigen::CwiseNullaryOp& matrix) { Node node; @@ -249,6 +265,10 @@ struct convert > template struct convert > { + /** \brief Encode a YAML Sequence (row-major) from an Eigen::CwiseBinaryOp + * \param matrix expression to be encoded + * \return the YAML sequence + */ static Node encode(const Eigen::CwiseBinaryOp& matrix) { Node node; @@ -272,6 +292,10 @@ struct convert > template struct convert > { + /** \brief Encode a YAML Sequence (row-major) from an Eigen::CwiseUnaryOp + * \param matrix expression to be encoded + * \return the YAML sequence + */ static Node encode(const Eigen::CwiseUnaryOp& matrix) { Node node; diff --git a/include/yaml-schema-cpp/yaml_generator.hpp b/include/yaml-schema-cpp/yaml_generator.hpp index dbcf4f5..82e6b83 100644 --- a/include/yaml-schema-cpp/yaml_generator.hpp +++ b/include/yaml-schema-cpp/yaml_generator.hpp @@ -1,20 +1,66 @@ +/** + * @file yaml_generator.hpp + * @brief Generation of template YAML files from schemas. + */ #pragma once #include "yaml-cpp/yaml.h" namespace yaml_schema_cpp { -const std::string red("\033[0;31m"); -const std::string yellow("\033[1;33m"); -const std::string reset("\033[0m"); +const std::string red = "\033[0;31m"; ///< ANSI escape code: red text (console output) +const std::string yellow = "\033[1;33m"; ///< ANSI escape code: yellow text (console output) +const std::string reset = "\033[0m"; ///< ANSI escape code: reset text color (console output) +/** + * @brief Generate a template YAML file from a schema. + * + * Each field of the template has a value (DEFAULT, the first of OPTIONS or a zero value) and a comment with its + * documentation, type, options and if it is optional (see generateYaml()). Fields with VALUE are not included. + * + * If filepath has no extension, YAML_EXTENSION is added. If the file already exists, a suffix "_N" is added to the + * file name to avoid overwriting it (a warning is printed). + * + * @param filepath path of the template file to be created + * @param name_schema name of the schema, with or without extension (without path) + * @param folders_schema folders where the schema files are searched (recursively) + * @param override if true, repeated specifications while flattening schemas are overridden, otherwise it is an error + * @return the path of the created template file + * @throws std::runtime_error if the folder of filepath does not exist, the schema cannot be generated (see + * generateYaml()) or the file cannot be created + */ std::string generateTemplate(std::string filepath, const std::string& name_schema, const std::vector& folders_schema, bool override = true); -YAML::Node generateYaml(std::string schema_file, const std::vector& folders_schema, bool override = true); +/** + * @brief Generate the template YAML node of a schema. + * + * The schema is found, loaded, flattened and checked, and converted to a YAML node (see schemaToYaml()). + * + * @param name_schema name of the schema, with or without extension (without path) + * @param folders_schema folders where the schema files are searched (recursively) + * @param override if true, repeated specifications while flattening schemas are overridden, otherwise it is an error + * @return template node, where each scalar contains the value (as YAML text) followed by a comment + * (e.g. "3 # OPTIONAL - DOC some doc - TYPE int") + * @throws std::runtime_error if the schema is not found, cannot be loaded or flattened, or is not valid + */ +YAML::Node generateYaml(std::string name_schema, const std::vector& folders_schema, bool override = true); +/** + * @brief Convert (recursively) a schema node into a template YAML node. + * + * Specifications with VALUE and scalar schema nodes are skipped (node_output is set to null). Custom types are + * generated from their schemas, "derived" types with a placeholder 'type' and 'follow', and arrays of unspecified + * size with 2 elements. + * + * @param node_schema schema node (flattened) + * @param node_output OUTPUT template node (see generateYaml()) + * @param folders_schema folders where the schema files are searched (recursively) + * @param override if true, repeated specifications while flattening schemas are overridden, otherwise it is an error + * @throws std::runtime_error if a custom type schema is not found or a schema node is not valid + */ void schemaToYaml(const YAML::Node& node_schema, YAML::Node& node_output, const std::vector& folders_schema, diff --git a/include/yaml-schema-cpp/yaml_schema.hpp b/include/yaml-schema-cpp/yaml_schema.hpp index e0fc148..b7246f5 100644 --- a/include/yaml-schema-cpp/yaml_schema.hpp +++ b/include/yaml-schema-cpp/yaml_schema.hpp @@ -1,3 +1,11 @@ +/** + * @file yaml_schema.hpp + * @brief Loading and checking of schema files, and validation of input YAML nodes against them. + * + * A schema is a YAML file (with extension SCHEMA_EXTENSION) that specifies the structure and contents of input YAML + * files. Nodes containing all REQUIRED_KEYS are "specifications" of an input field, the rest of the nodes are maps of + * other nodes. See the README for a description of the schema syntax. + */ #pragma once #include @@ -9,59 +17,213 @@ #include "yaml-schema-cpp/type_check.hpp" #include "yaml-schema-cpp/yaml_utils.hpp" +/** + * @brief Specification and validation of YAML files via schema files. + */ namespace yaml_schema_cpp { -static std::string SCHEMA_EXTENSION = ".schema"; -static std::string YAML_EXTENSION = ".yaml"; -static std::string SCHEMA_PREFIX = "_"; -static std::string TYPE = SCHEMA_PREFIX + "type"; -static std::string MANDATORY = SCHEMA_PREFIX + "mandatory"; -static std::string DOC = SCHEMA_PREFIX + "doc"; -static std::string OPTIONS = SCHEMA_PREFIX + "options"; -static std::string DEFAULT = SCHEMA_PREFIX + "default"; -static std::string VALUE = SCHEMA_PREFIX + "value"; -static std::string BASE = SCHEMA_PREFIX + "base"; -static std::list RESERVED_KEYS{TYPE, MANDATORY, DOC, OPTIONS, DEFAULT, BASE}; -static std::list REQUIRED_KEYS{TYPE, MANDATORY, DOC}; - -YAML::Node loadSchema(std::string schema_file, +/// Extension of schema files +static const std::string SCHEMA_EXTENSION = ".schema"; +/// Extension of input YAML files +static const std::string YAML_EXTENSION = ".yaml"; +/// Prefix of all keys reserved for specifications +static const std::string SCHEMA_PREFIX = "_"; +/// Specification key (required): type of the field +static const std::string TYPE = SCHEMA_PREFIX + "type"; +/// Specification key (required): if the field is mandatory, bool or expression (see expression.hpp) +static const std::string MANDATORY = SCHEMA_PREFIX + "mandatory"; +/// Specification key (required): documentation of the field +static const std::string DOC = SCHEMA_PREFIX + "doc"; +/// Specification key (optional): sequence of accepted values +static const std::string OPTIONS = SCHEMA_PREFIX + "options"; +/// Specification key (optional): value added if the field is missing (only if not mandatory) +static const std::string DEFAULT = SCHEMA_PREFIX + "default"; +/// Specification key (optional): value fixed by the schema (the field cannot be changed in the input) +static const std::string VALUE = SCHEMA_PREFIX + "value"; +/// Specification key (required for "derived" types): base schema that the derived type should follow +static const std::string BASE = SCHEMA_PREFIX + "base"; +/// Keys reserved for specifications (not allowed in nodes that are not specifications) +static const std::list RESERVED_KEYS{TYPE, MANDATORY, DOC, OPTIONS, DEFAULT, VALUE, BASE}; +/// Keys required for a node to be a specification +static const std::list REQUIRED_KEYS{TYPE, MANDATORY, DOC}; + +/** + * @brief Find, load, flatten and check a schema. + * + * The schema file is searched recursively in folders_schema (see findSchema()). Then, all 'follow' keys are resolved + * (see flattenNode()) and the resulting schema is checked (see checkSchema()). + * + * @param name_schema name of the schema file, with or without extension (without path) + * @param folders_schema folders where the schema files are searched (recursively) + * @param log OUTPUT stream where the path of the schema file and the errors (if any) are written + * @param override if true, repeated specifications while flattening are overridden, otherwise it is an error + * @return the flattened schema node, or an undefined node if any of the steps failed + */ +YAML::Node loadSchema(std::string name_schema, const std::vector& folders_schema, std::stringstream& log, bool override = true); -void checkSchema(const YAML::Node& node_schema, - const std::string& field, - const YAML::Node& node_schema_parent, - const std::vector& folders_schema); -void checkSchemaValue(const YAML::Node& node_schema, - const std::string& node_field, - const YAML::Node& node_schema_parent, - const std::vector& folders_schema); -void checkSchemaDefault(const YAML::Node& node_schema, - const std::string& node_field, - const YAML::Node& node_schema_parent, - const std::vector& folders_schema); -void checkSchemaOptions(const YAML::Node& node_schema, - const std::string& node_field, - const YAML::Node& node_schema_parent, - const std::vector& folders_schema); +/** + * @brief Check recursively that a (flattened) schema node is valid. + * + * For each specification node, it checks that: + * - TYPE, DOC and BASE (required if TYPE is "derived" or "derived[..]") are strings, + * - MANDATORY is a bool or a valid expression (see checkExpression()), + * - VALUE, DEFAULT and OPTIONS (if defined) are valid (see checkSchemaValue(), checkSchemaDefault() and + * checkSchemaOptions()). + * + * Nodes that are not specifications should be maps without any of the RESERVED_KEYS. Scalar and null nodes are + * skipped. + * + * @param node_schema schema node to be checked + * @param field name of the field (used in error messages) + * @param node_schema_parent parent node of node_schema (used to check expressions) + * @param folders_schema folders where the schema files are searched (recursively) + * @throws std::runtime_error if the schema is not valid + */ +void checkSchema(const YAML::Node& node_schema, + const std::string& field, + const YAML::Node& node_schema_parent, + const std::vector& folders_schema); + +/** + * @brief Check the VALUE of a specification node. + * + * VALUE should pass applySchema() for the TYPE (or BASE for "derived" types, see getCheckType()), and MANDATORY should + * be false. NOTE: applySchema() may complete VALUE with the defaults of its type. + * + * @param node_schema specification node containing VALUE + * @param node_field name of the field (used in error messages) + * @param folders_schema folders where the schema files are searched (recursively) + * @throws std::runtime_error if VALUE is not valid + */ +void checkSchemaValue(const YAML::Node& node_schema, + const std::string& node_field, + const std::vector& folders_schema); + +/** + * @brief Check the DEFAULT of a specification node. + * + * MANDATORY should not be true, and DEFAULT should pass applySchema() for the TYPE (or BASE for "derived" types, see + * getCheckType()). NOTE: applySchema() may complete DEFAULT with the defaults of its type. + * + * @param node_schema specification node containing DEFAULT + * @param node_field name of the field (used in error messages) + * @param folders_schema folders where the schema files are searched (recursively) + * @throws std::runtime_error if DEFAULT is not valid + */ +void checkSchemaDefault(const YAML::Node& node_schema, + const std::string& node_field, + const std::vector& folders_schema); + +/** + * @brief Check the OPTIONS of a specification node. + * + * OPTIONS should be a sequence, each option should pass applySchema() for the TYPE (or BASE for "derived" types, see + * getCheckType()), and DEFAULT and VALUE (if defined) should be one of the options. + * + * @param node_schema specification node containing OPTIONS + * @param node_field name of the field (used in error messages) + * @param folders_schema folders where the schema files are searched (recursively) + * @throws std::runtime_error if OPTIONS is not valid + */ +void checkSchemaOptions(const YAML::Node& node_schema, + const std::string& node_field, + const std::vector& folders_schema); + +/** + * @brief Load, flatten and check all schema files found (recursively) in the given folders. + * + * The result of each schema is printed in std::cout: errors are always printed, the rest only if verbose. + * Folders that do not exist are skipped. + * + * @param folders_schema folders where the schema files are searched (recursively), also used to resolve 'follow' + * @param verbose if true, prints the name of each schema file and "OK!" for the valid ones + * @param override if true, repeated specifications while flattening are overridden, otherwise it is an error + * @return true if all schemas are valid + */ bool validateAllSchemas(const std::vector& folders_schema, bool verbose, bool override = true); +/** + * @brief Validate an input node against a type and complete it. + * + * The type can be: + * - a trivial type (see isTrivialType()), checked with tryNodeAs(). Null values are not accepted for string types. + * - an array type (e.g. "int[]", "MyType[3]"): the input should be a sequence (of the given size, if specified) and + * each element is validated against the lower element type (see getLowerElementType()). + * - otherwise, the name of a schema: it is loaded (see loadSchema()) and applied (see applySchemaRecursive()). + * + * Applying a schema completes the input node: missing fields with DEFAULT (and not mandatory) or VALUE are added. + * + * @param node_input input node to be validated (and completed) + * @param type type of node_input: trivial type, array type or name of a schema + * @param folders folders where the schema files are searched (recursively) + * @param log OUTPUT stream where the errors are written + * @param acc_field accumulated path of the field (e.g. "map1/param2[0]"), used in error messages ("" at top level) + * @param override if true, repeated specifications while flattening schemas are overridden, otherwise it is an error + * @return true if node_input is valid + */ bool applySchema(YAML::Node& node_input, - const std::string& name_schema, + const std::string& type, const std::vector& folders, std::stringstream& log, const std::string& acc_field, bool override = true); +/** + * @brief Validate an input node against a schema node and complete it (recursively). + * + * If node_schema is a specification: + * - if node_input is defined, it is validated against TYPE (see applySchema() and applySchemaDerived()). If valid, + * it should be equal to VALUE and be one of the OPTIONS (if defined). + * - otherwise: if VALUE is defined, node_input is set to VALUE. If not, an error is reported if MANDATORY (bool or + * expression evaluated with evalExpression()) is true, or node_input is set to DEFAULT (if defined). + * + * Otherwise (map of other schema nodes), node_input should be a map (it is created if not defined) and each child is + * validated recursively. The children with an expression in MANDATORY are processed after the rest, so that the + * values and defaults of the parameters involved in the expressions are already set. + * + * Scalar schema nodes are ignored. + * + * @param node_input input node to be validated. If not defined, it should be obtained via node_input_parent[key], so + * that setting it (VALUE, DEFAULT or empty map) adds the key to node_input_parent. + * @param node_input_parent parent node of node_input (used to evaluate expressions) + * @param node_schema schema node: either a specification or a map of other schema nodes + * @param folders folders where the schema files are searched (recursively) + * @param log OUTPUT stream where the errors are written + * @param acc_field accumulated path of the field (e.g. "map1/param2[0]"), used in error messages + * @param override if true, repeated specifications while flattening schemas are overridden, otherwise it is an error + * @param node_schema_parent parent node of node_schema (used to get the types of the parameters in expressions). + * Undefined if not available. + * @return true if node_input is valid + */ bool applySchemaRecursive(YAML::Node& node_input, YAML::Node& node_input_parent, const YAML::Node& node_schema, const std::vector& folders, std::stringstream& log, const std::string& acc_field, - bool override); + bool override, + const YAML::Node& node_schema_parent = YAML::Node(YAML::NodeType::Undefined)); +/** + * @brief Validate an input node against a "derived" type specification and complete it. + * + * If TYPE is an array ("derived[]", "derived[3]", ...), node_input should be a sequence (of the given size, if + * specified) and each element is validated recursively. Otherwise, node_input should be a map containing the key + * 'type' (string) with the name of the derived schema. It is validated against the derived schema and then against + * the BASE schema. + * + * @param node_input input node to be validated (and completed) + * @param node_input_parent parent node of node_input + * @param node_schema specification node with a "derived" TYPE and BASE + * @param folders folders where the schema files are searched (recursively) + * @param log OUTPUT stream where the errors are written + * @param acc_field accumulated path of the field (e.g. "map1/param2[0]"), used in error messages + * @param override if true, repeated specifications while flattening schemas are overridden, otherwise it is an error + * @return true if node_input is valid + */ bool applySchemaDerived(YAML::Node& node_input, YAML::Node& node_input_parent, const YAML::Node& node_schema, @@ -70,16 +232,70 @@ bool applySchemaDerived(YAML::Node& node_input, const std::string& acc_field, bool override); +/** + * @brief Check if an input node is equal to any of the options (see compare()). + * + * @param input_node input node + * @param options_node sequence of options + * @param type type used to compare the nodes + * @param folders_schema folders where the schema files are searched (recursively) + * @return true if input_node is equal to any of the options + */ bool isInOptions(const YAML::Node& input_node, const YAML::Node& options_node, const std::string& type, const std::vector& folders_schema = {}); +/** + * @brief Check if a schema node has any of the RESERVED_KEYS. + * @param node_schema schema node + * @return true if node_schema is a map containing any of the RESERVED_KEYS + */ bool hasAnyReservedKey(const YAML::Node& node_schema); + +/** + * @brief Check if a schema node is a specification. + * @param node_schema schema node + * @return true if node_schema is a map containing all REQUIRED_KEYS + */ bool isSpecification(const YAML::Node& node_schema); + +/** + * @brief Check if a specification node has an array type (see isArrayType()). + * @param node_schema specification node + * @return true if TYPE is an array type + * @throws std::runtime_error if node_schema does not have TYPE + */ bool isSequenceSchema(const YAML::Node& node_schema); + +/** + * @brief Check if a specification node has an array type (see isArrayType()). + * @param node_schema specification node + * @param size OUTPUT (if array type): size of the array, 0 if not specified + * @return true if TYPE is an array type + * @throws std::runtime_error if node_schema does not have TYPE + */ bool isSequenceSchema(const YAML::Node& node_schema, size_t& size); +/** + * @brief Add a schema node to node[key], merging it with the existing one (used to flatten schemas). + * + * - if node[key] does not exist, it is set to value. + * - if node[key] is a specification or a scalar or a sequence, it is replaced by value if override, otherwise an + * exception is thrown. + * - otherwise (map), all children of value are added recursively. + * + * If parent_path is not empty and the resulting node[key] is a relative path (see isRelativePath()), parent_path is + * prepended. + * + * @param node schema node where value is added + * @param key key in node where value is added + * @param value schema node to be added + * @param override if true, existing specifications are overridden, otherwise it is an error + * @param parent_path path prepended to relative path values ("" to leave them unchanged) + * @throws std::runtime_error if a specification exists and not override, or node[key] has reserved keys but is not + * a specification + */ void addNodeSchema(YAML::Node& node, const std::string& key, const YAML::Node& value, diff --git a/include/yaml-schema-cpp/yaml_server.hpp b/include/yaml-schema-cpp/yaml_server.hpp index 04c699d..e6ae868 100644 --- a/include/yaml-schema-cpp/yaml_server.hpp +++ b/include/yaml-schema-cpp/yaml_server.hpp @@ -1,3 +1,7 @@ +/** + * @file yaml_server.hpp + * @brief YamlServer: main interface to load input YAML files and validate them against schemas. + */ #pragma once #include @@ -7,36 +11,133 @@ namespace yaml_schema_cpp { +/** + * @brief Loads an input YAML (file or node) and validates and completes it against schemas. + * + * Example: + * @code + * YamlServer server({"/path/to/schemas"}, "/path/to/input.yaml"); + * if (not server.applySchema("MySchema")) + * std::cout << server.getLog() << std::endl; + * YAML::Node node = server.getNode(); + * @endcode + */ class YamlServer { public: + /** + * @brief Constructor without schema folders and without input. + * @param override if true, repeated keys while flattening (input and schemas) are overridden, otherwise it is an + * error + */ YamlServer(bool override = true); + + /** + * @brief Constructor without input. + * @param folders_schema folders where the schema files are searched (recursively) + * @param override if true, repeated keys while flattening (input and schemas) are overridden, otherwise it is an + * error + */ YamlServer(const std::vector& folders_schema, bool override = true); + + /** + * @brief Constructor loading an input YAML file (see loadYaml()). + * @param folders_schema folders where the schema files are searched (recursively) + * @param path_input path of the input YAML file + * @param override if true, repeated keys while flattening (input and schemas) are overridden, otherwise it is an + * error + * @throws std::runtime_error if the file does not exist or cannot be flattened + */ YamlServer(const std::vector& folders_schema, const std::string& path_input, bool override = true); + /** + * @brief Constructor loading an input YAML file (see loadYaml()). + * + * Overload for string literals (otherwise, they would be converted to bool, selecting the constructor without + * input). + * + * @param folders_schema folders where the schema files are searched (recursively) + * @param path_input path of the input YAML file + * @param override if true, repeated keys while flattening (input and schemas) are overridden, otherwise it is an + * error + * @throws std::runtime_error if the file does not exist or cannot be flattened + */ + YamlServer(const std::vector& folders_schema, const char* path_input, bool override = true); + + /** + * @brief Validate and complete the input node against a schema (see yaml_schema_cpp::applySchema()). + * + * Missing optional fields with default values and fields with values fixed in the schema are added to the input + * node. Null values are not accepted for string fields. The log is reset and filled with the errors found. + * + * @param name_schema name of the schema, with or without extension (without path) + * @return true if the input node is valid. Otherwise, the errors can be retrieved with getLog(). + */ bool applySchema(const std::string& name_schema); - void addFolderSchema(const std::vector& folders_schema, bool before = false); - void addFolderSchema(const std::string& folder_schema, bool before = false); + /** + * @brief Add folders where the schema files are searched (recursively). + * @param folders_schema folders to be added + * @param before if true, added at the beginning (searched first), otherwise at the end + */ + void addFolderSchema(const std::vector& folders_schema, bool before = false); + + /** + * @brief Add a folder where the schema files are searched (recursively). + * @param folder_schema folder to be added + * @param before if true, added at the beginning (searched first), otherwise at the end + */ + void addFolderSchema(const std::string& folder_schema, bool before = false); + + /** + * @brief Get the folders where the schema files are searched. + * @return the schema folders, in search order + */ std::vector getFolderSchema() const; + /** + * @brief Load an input YAML file. + * + * The 'follow' keys are resolved, and relative paths (values starting with "./" or "../") are prepended with the + * folder of the file where they are defined (see flattenNode()). + * + * @param path_input path of the input YAML file (absolute or relative to the current working directory) + * @throws std::runtime_error if the file does not exist or cannot be flattened (YAML::Exception if it cannot be + * parsed) + */ void loadYaml(const std::string& path_input); + + /** + * @brief Set the input node (a copy of it). + * + * NOTE: Unlike loadYaml(), the 'follow' keys and relative paths are not resolved. + * + * @param _node_input input node + */ void setYaml(const YAML::Node _node_input); + /** + * @brief Get the log of the last call to applySchema(). + * @return the log, including the errors found (if any) + */ std::string getLog() const; + /** + * @brief Get the input node (completed by applySchema(), if called). + * @return a copy of the input node + */ YAML::Node getNode() const; private: - std::vector folders_schema_; + std::vector folders_schema_; ///< folders where the schema files are searched (recursively) - std::string path_input_; + std::string path_input_; ///< path of the input YAML file (empty if set via setYaml()) - std::stringstream log_; + std::stringstream log_; ///< log of the last call to applySchema() - YAML::Node node_input_; + YAML::Node node_input_; ///< input node - bool override_; + bool override_; ///< if true, repeated keys while flattening are overridden, otherwise it is an error }; } // namespace yaml_schema_cpp diff --git a/include/yaml-schema-cpp/yaml_utils.hpp b/include/yaml-schema-cpp/yaml_utils.hpp index 813f389..6d2147e 100644 --- a/include/yaml-schema-cpp/yaml_utils.hpp +++ b/include/yaml-schema-cpp/yaml_utils.hpp @@ -1,3 +1,7 @@ +/** + * @file yaml_utils.hpp + * @brief Utilities: flattening of YAML nodes ('follow'), file search, type strings, logging and node comparison. + */ #pragma once #include @@ -7,61 +11,175 @@ namespace yaml_schema_cpp { -// 'follow' behavior: -// -> schema node (is_schema = true): only allowed single file names. It will be searched inside folders specified by -// 'folders' -// -> input node (is_schema = false): path to file allowed (either relative or absolute). Relative paths will be done -// with respect to the first item specified in folders. No search will be performed. +/** + * @brief Flatten a node: resolve recursively all 'follow' keys, replacing them by the contents of the followed files. + * + * The value of 'follow' can be a file or a sequence of files: + * - a schema file (extension SCHEMA_EXTENSION, or no extension): only the file name, it is searched recursively in + * schema_folders (see findFileRecursive()). + * - an input YAML file (extension YAML_EXTENSION): absolute path or relative to current_folder. No search is done. + * + * The contents of the followed file are merged with the rest of the node (see addNodeSchema() and addNodeYaml()). + * + * In input YAML nodes (is_schema false), relative paths (scalar values starting with "./" or "../") are prepended + * with the folder of the file where they are defined (current_folder for this node). + * + * @param node INPUT/OUTPUT node to be flattened + * @param current_folder folder of the file containing node + * @param schema_folders folders where the followed schema files are searched (recursively) + * @param is_schema true if node is a schema node, false if it is an input YAML node + * @param override if true, repeated keys are overridden, otherwise it is an error + * @throws std::runtime_error if a followed file is not found, has a wrong extension or cannot be merged, or if there + * is a 'follow' cycle (a file that follows, directly or indirectly, itself) + */ void flattenNode(YAML::Node& node, std::string current_folder, std::vector schema_folders, bool is_schema, bool override); +/** + * @brief Flatten a map node (see flattenNode()). + * @param node INPUT/OUTPUT map node to be flattened + * @param current_folder folder of the file containing node + * @param schema_folders folders where the followed schema files are searched (recursively) + * @param is_schema true if node is a schema node, false if it is an input YAML node + * @param override if true, repeated keys are overridden, otherwise it is an error + * @throws std::runtime_error (see flattenNode()) + */ void flattenMap(YAML::Node& node, std::string current_folder, std::vector schema_folders, bool is_schema, bool override); +/** + * @brief Flatten all elements of a sequence node (see flattenNode()). + * @param node INPUT/OUTPUT sequence node to be flattened + * @param current_folder folder of the file containing node + * @param schema_folders folders where the followed schema files are searched (recursively) + * @param is_schema true if node is a schema node, false if it is an input YAML node + * @param override if true, repeated keys are overridden, otherwise it is an error + * @throws std::runtime_error (see flattenNode()) + */ void flattenSequence(YAML::Node& node, std::string current_folder, std::vector schema_folders, bool is_schema, bool override); +/** + * @brief Load the file(s) of a 'follow' value, flatten them and add their contents to a node (see flattenNode()). + * @param node node where the contents are added + * @param node_follow value of 'follow': a file name/path or a sequence of them + * @param current_folder folder of the file containing the 'follow' key + * @param schema_folders folders where the followed schema files are searched (recursively) + * @param override if true, repeated keys are overridden, otherwise it is an error + * @throws std::runtime_error (see flattenNode()) + */ void insertNodes(YAML::Node& node, const YAML::Node& node_follow, std::string current_folder, std::vector schema_folders, bool override); +/** + * @brief Write an error in the log: "ERROR in '': ", followed by the schema specification (if any). + * @param log OUTPUT stream where the error is written + * @param _acc_field accumulated path of the field (e.g. "map1/param2[0]") + * @param _node_schema schema node of the field (undefined to not write it) + * @param _error_message error message + */ void writeErrorToLog(std::stringstream& log, const std::string& _acc_field, const YAML::Node _node_schema, const std::string& _error_message); +/** + * @brief Write (recursively) a schema node in the log in a readable way. + * @param log OUTPUT stream where the schema node is written + * @param _acc_field accumulated path of the field (e.g. "map1/param2") + * @param _node_schema schema node to be written + * @param _tabs indentation prepended to each line + */ void writeNodeSchemaToLog(std::stringstream& log, const std::string& _acc_field, const YAML::Node _node_schema, std::string _tabs = ""); +/** + * @brief Add an input YAML node to node[key], merging it with the existing one (used to flatten input YAML nodes). + * + * - if node[key] does not exist, it is set to value. + * - if value is a scalar, node[key] is replaced if override, otherwise an exception is thrown. + * - if value is a sequence, its elements are appended to node[key]. + * - if value is a map, all its children are added recursively. + * + * If parent_path is not empty and the resulting node[key] is a relative path (see isRelativePath()), parent_path is + * prepended. + * + * @param node node where value is added + * @param key key in node where value is added + * @param value node to be added + * @param override if true, existing scalars are overridden, otherwise it is an error + * @param parent_path path prepended to relative path values ("" to leave them unchanged) + * @throws std::runtime_error if a scalar exists and not override, or value is of unknown type + */ void addNodeYaml(YAML::Node& node, const std::string& key, const YAML::Node& value, bool override, std::string parent_path = ""); +/** + * @brief Check if a string is a relative path. + * @param value string + * @return true if value starts with "./" or "../" + */ +bool isRelativePath(const std::string& value); + +/** + * @brief Search recursively a file in folders (in order). + * @param name_with_extension name of the file (with extension, without path) + * @param folders folders where the file is searched (recursively). Folders that do not exist are skipped. + * @return the path of the first regular file found with the given name + * @throws std::runtime_error if the file is not found (the message lists the folders) or a folder cannot be iterated + */ std::string findFileRecursive(const std::string& name_with_extension, const std::vector& folders); +/** + * @brief Search recursively a schema file in folders (see findFileRecursive()). + * @param name_schema name of the schema file, with or without extension SCHEMA_EXTENSION (without path) + * @param folders folders where the schema file is searched (recursively) + * @param log OUTPUT stream where the error is written (if any) + * @return the path of the schema file, or an empty string if not found or wrong extension + */ std::string findSchema(std::string name_schema, const std::vector& folders, std::ostream& log = std::cout); +/** + * @brief Get the names of all schema files found recursively in folders. + * @param root_folders folders where the schema files are searched (recursively). Folders that do not exist are + * skipped. + * @return the file names (without path) of all files with extension SCHEMA_EXTENSION + */ std::vector getAllSchemas(const std::vector& root_folders); +/** + * @brief Find (recursively, in maps and sequences) all the nodes with a given key. + * @param root_node node where the keys are searched + * @param key key to be found + * @return the values of all the occurrences of key + */ std::list findNodesWithKey(const YAML::Node root_node, const std::string& key); +/** + * @brief Convert a sequence node (of scalars or sequences) into a flow-style string (e.g. "[1, 2, [a, b]]"). + * @param node sequence node + * @return the string + * @throws std::runtime_error if node is not a sequence (YAML::Exception if any element is a map) + */ std::string sequenceToString(const YAML::Node& node); /** @@ -69,6 +187,7 @@ std::string sequenceToString(const YAML::Node& node); * Searches for the first [ and ]. * @param type_str INPUT string containing type * @returns either type_str is a sequence type ('[' and ']' found) + * @throws std::runtime_error if the brackets are malformed or the size is not a positive integer */ bool isArrayType(const std::string& type_str); @@ -76,9 +195,10 @@ bool isArrayType(const std::string& type_str); * @brief evaluates string to check if it is an array type (contains [ ]) * Searches for the first [ and ]. * @param type_str INPUT string containing type - * @param size OUTPUT (if sequence type): the size of the array + * @param size OUTPUT (if sequence type): the size of the array, 0 if not specified * (example: for double[3][5] returns 3) * @returns either type_str is a sequence type ('[' and ']' found) + * @throws std::runtime_error if the brackets are malformed or the size is not a positive integer */ bool isArrayType(const std::string& type_str, size_t& size); @@ -105,74 +225,34 @@ std::string getLowestElementType(const std::string type_str); /** * @brief Get the type to be used to check from a schema node. + * @param node specification node * @returns If TYPE does not start by "derived", returns TYPE. - * If TYPE start by "derived", substitutes "derived" by BASE + * If TYPE start by "derived", substitutes "derived" by BASE (e.g. "derived[]" -> "MyBase[]") + * @throws std::runtime_error if node does not have TYPE, or BASE (if derived) */ std::string getCheckType(const YAML::Node& node); -/** - * @brief Compare nodes of a given type - * - */ -#define COMPARE_TYPE(TypeName) \ - if (type == #TypeName) \ - { \ - try \ - { \ - return node1.as() == node2.as(); \ - } \ - catch (const std::exception& e) \ - { \ - return false; \ - } \ - } - -#if _EIGEN_FOUND == 1 -#define COMPARE_EIGEN(size) \ - if (type == "Vector" #size "d" or type == "Eigen::Vector" #size "d") \ - { \ - try \ - { \ - return (node1.as >() - node2.as >()) \ - .cwiseAbs() \ - .maxCoeff() < 1e-9; \ - } \ - catch (const std::exception& e) \ - { \ - return false; \ - } \ - } \ - if (type == "Matrix" #size "d" or type == "Eigen::Matrix" #size "d") \ - { \ - try \ - { \ - return (node1.as >() - node2.as >()) \ - .cwiseAbs() \ - .maxCoeff() < 1e-9; \ - } \ - catch (const std::exception& e) \ - { \ - return false; \ - } \ - } -#endif - /** * @brief Compare two nodes interpreting all scalars automatically. - * First try as integers, second as doubles, finally as strings. + * First try as integers, second as doubles, finally as strings. Maps and sequences are compared recursively. + * Two undefined nodes or two null nodes are equal. * - * @param node1, node2 nodes to be compared + * @param node1 node to be compared + * @param node2 node to be compared * @return if both nodes are equal */ bool compareNodesAutoType(const YAML::Node& node1, const YAML::Node& node2); /** * @brief Compare two nodes interpreted as a given type + * Array types are compared element by element. Trivial types are compared with compareTrivial() and the rest with + * compareNonTrivial(). * - * @param node1, node2 nodes to be compared + * @param node1 node to be compared + * @param node2 node to be compared * @param type string containing the type * @param folders_schema string vector containing folders where to search for schema files - * @return if both nodes are equal + * @return if both nodes are equal (false if any of them is undefined or null) */ bool compare(const YAML::Node& node1, const YAML::Node& node2, @@ -180,20 +260,25 @@ bool compare(const YAML::Node& node1, const std::vector& folders_schema); /** * @brief Compare two nodes interpreted as a given type (trivial: no schema needed) + * Eigen types are compared with a tolerance of 1e-9. * - * @param node1, node2 nodes to be compared + * @param node1 node to be compared + * @param node2 node to be compared * @param type string containing the type - * @return if both nodes are equal + * @return if both nodes are equal (false if any of them is undefined, null or cannot be converted) + * @throws std::runtime_error if type is not a trivial type */ bool compareTrivial(const YAML::Node& node1, const YAML::Node& node2, const std::string& type); /** * @brief Compare two nodes interpreted as a given type (nontrivial: schema needed) + * The schema is loaded and flattened (but not checked) and the nodes are compared with compareNonTrivialSchema(). * - * @param node1, node2 nodes to be compared - * @param type string containing the type + * @param node1 node to be compared + * @param node2 node to be compared + * @param type string containing the type (name of the schema) * @param folders_schema string vector containing folders where to search for schema files - * @return if both nodes are equal + * @return if both nodes are equal (false if any of them is undefined or null, or the schema cannot be loaded) */ bool compareNonTrivial(const YAML::Node& node1, const YAML::Node& node2, @@ -202,8 +287,11 @@ bool compareNonTrivial(const YAML::Node& node1, /** * @brief Compare two nodes interpreted as a given type using a schema + * Only the fields specified in the schema are compared. Optional fields (MANDATORY false or expression) missing in + * both nodes are considered equal. * - * @param node1, node2 nodes to be compared + * @param node1 node to be compared + * @param node2 node to be compared * @param node_schema schema specifying structure and types of the nodes * @param folders_schema string vector containing folders where to search for schema files * @return if both nodes are equal @@ -214,7 +302,9 @@ bool compareNonTrivialSchema(const YAML::Node& node1, const std::vector& folders_schema); /** * @brief Return string with zero value of given type + * Arrays of unspecified size (and dynamic Eigen types) have 3 elements. * @param type string containing the type + * @return the zero value as YAML text (e.g. "0.0", "[0.0, 0.0]", "false"), or "type unknown!" for non-trivial types */ std::string getZeroString(const std::string& type); diff --git a/src/expression.cpp b/src/expression.cpp index 1117b65..4729e03 100644 --- a/src/expression.cpp +++ b/src/expression.cpp @@ -1,4 +1,7 @@ #include "yaml-schema-cpp/expression.hpp" + +#include + #include "yaml-schema-cpp/exprtk/exprtk.hpp" #include "yaml-schema-cpp/type_check.hpp" #include "yaml-schema-cpp/yaml_schema.hpp" @@ -10,25 +13,54 @@ typedef exprtk::parser parser_t; typedef exprtk::symbol_table symbol_table_t; typedef typename parser_t::unknown_symbol_resolver usr_t; +// Types that are handled as strings in expressions +static bool isExpressionStringType(const std::string& type) +{ + return isStringType(type) or type == "char"; +} + struct yaml_USR : public parser_t::unknown_symbol_resolver { - YAML::Node node_; + const YAML::Node node_; + const YAML::Node schema_; - yaml_USR(const YAML::Node& _node) : usr_t(usr_t::e_usrmode_extended), node_(_node) {} + yaml_USR(const YAML::Node& _node, const YAML::Node& _schema) + : usr_t(usr_t::e_usrmode_extended), node_(_node), schema_(_schema) + { + } virtual bool process(const std::string& unknown_symbol, symbol_table_t& symbol_table, std::string& error_message) { bool result = false; // inexistent - if (not node_[unknown_symbol]) + if (not node_.IsMap() or not node_[unknown_symbol]) { error_message = "Indeterminable symbol type."; + return false; } + + const YAML::Node node_symbol = node_[unknown_symbol]; + + // type specified in schema (if available), otherwise deduced from the value + std::string type; + if (schema_.IsDefined() and schema_.IsMap() and isSpecification(schema_[unknown_symbol])) + type = schema_[unknown_symbol][TYPE].as(); + + bool is_bool = type.empty() ? tryNodeAs(node_symbol, "bool") : type == "bool"; + bool is_string = type.empty() ? false : isExpressionStringType(type); + bool is_scalar = + type.empty() ? tryNodeAs(node_symbol, "double") : (not is_bool and not is_string and isBasicType(type)); + // boolean - else if (tryNodeAs(node_[unknown_symbol], "bool")) + if (is_bool) { - double value = node_[unknown_symbol].as() ? 1 : 0; + if (not tryNodeAs(node_symbol, "bool")) + { + error_message = "Parameter " + unknown_symbol + " cannot be interpreted as bool."; + return false; + } + double value = node_symbol.as() ? 1 : 0; result = symbol_table.create_variable(unknown_symbol, value); if (not result) @@ -37,9 +69,14 @@ struct yaml_USR : public parser_t::unknown_symbol_resolver } } // scalar - else if (tryNodeAs(node_[unknown_symbol], "double")) + else if (is_scalar) { - double value = node_[unknown_symbol].as(); + if (not tryNodeAs(node_symbol, "double")) + { + error_message = "Parameter " + unknown_symbol + " cannot be interpreted as a number."; + return false; + } + double value = node_symbol.as(); result = symbol_table.create_variable(unknown_symbol, value); if (not result) @@ -48,9 +85,14 @@ struct yaml_USR : public parser_t::unknown_symbol_resolver } } // string (last one since it is always possible to take as string) - else if (tryNodeAs(node_[unknown_symbol], "string")) + else if (is_string or (type.empty() and tryNodeAs(node_symbol, "string"))) { - std::string string_val = node_[unknown_symbol].as(); + if (not node_symbol.IsScalar()) + { + error_message = "Parameter " + unknown_symbol + " cannot be interpreted as string."; + return false; + } + std::string string_val = node_symbol.as(); result = symbol_table.create_stringvar(unknown_symbol, string_val); if (not result) @@ -68,7 +110,7 @@ struct yaml_USR : public parser_t::unknown_symbol_resolver struct schema_USR : public parser_t::unknown_symbol_resolver { - YAML::Node schema_; + const YAML::Node schema_; schema_USR(const YAML::Node& _schema) : usr_t(usr_t::e_usrmode_extended), schema_(_schema) {} @@ -99,24 +141,24 @@ struct schema_USR : public parser_t::unknown_symbol_resolver error_message = "checkExpression: The parameter '" + unknown_symbol + "' is not mandatory (expressions with optional parameters not implemented yet)"; } - // scalar - else if (isBasicType(schema_[unknown_symbol][TYPE].as())) + // string + else if (isExpressionStringType(schema_[unknown_symbol][TYPE].as())) { - result = symbol_table.create_variable(unknown_symbol, 0); // initialize to 0/false, just checking syntax + result = symbol_table.create_stringvar(unknown_symbol, ""); if (not result) { - error_message = "Failed to create variable " + unknown_symbol; + error_message = "Failed to create string variable " + unknown_symbol; } } - // string - else if (isStringType(schema_[unknown_symbol][TYPE].as())) + // scalar + else if (isBasicType(schema_[unknown_symbol][TYPE].as())) { - result = symbol_table.create_stringvar(unknown_symbol, ""); + result = symbol_table.create_variable(unknown_symbol, 0); // initialize to 0/false, just checking syntax if (not result) { - error_message = "Failed to create string variable " + unknown_symbol; + error_message = "Failed to create variable " + unknown_symbol; } } else @@ -129,12 +171,12 @@ struct schema_USR : public parser_t::unknown_symbol_resolver bool isExpression(const YAML::Node& node) { - return node.as().front() == '$'; + return node.IsDefined() and node.IsScalar() and isExpression(node.Scalar()); } bool isExpression(const std::string& expression) { - return expression.front() == '$'; + return not expression.empty() and expression.front() == '$'; } bool checkExpression(const YAML::Node& node_expression, const YAML::Node& node_schema_parent, std::string& err) @@ -157,7 +199,6 @@ bool checkExpression(const YAML::Node& node_expression, const YAML::Node& node_s parser_t parser; expression.register_symbol_table(symbol_table); parser.enable_unknown_symbol_resolver(&schema_usr); - parser.compile(expression_str, expression); // check exprtk syntax validity if (not parser.compile(expression_str, expression)) @@ -176,21 +217,22 @@ bool checkExpression(const YAML::Node& node_expression, const YAML::Node& node_s return true; } -bool evalExpression(std::string expression_str, const YAML::Node& node_input_parent) +bool evalExpression(std::string expression_str, + const YAML::Node& node_input_parent, + const YAML::Node& node_schema_parent) { assert(isExpression(expression_str) and "evalExpression: expression does not contain an expression"); // Preprocess: remove '$' preProcessExpression(expression_str); - // Parser with our symbol resolver for schemas + // Parser with our symbol resolver for input yaml nodes expression_t expression; symbol_table_t symbol_table; - yaml_USR yaml_usr(node_input_parent); + yaml_USR yaml_usr(node_input_parent, node_schema_parent); parser_t parser; expression.register_symbol_table(symbol_table); parser.enable_unknown_symbol_resolver(&yaml_usr); - parser.compile(expression_str, expression); // check exprtk syntax validity if (not parser.compile(expression_str, expression)) @@ -213,7 +255,7 @@ bool evalExpression(std::string expression_str, const YAML::Node& node_input_par void preProcessExpression(std::string& expression_str) { // remove first '$' if there - if (expression_str.front() == '$') expression_str.erase(expression_str.begin()); + if (isExpression(expression_str)) expression_str.erase(expression_str.begin()); } } // namespace yaml_schema_cpp diff --git a/src/type_check.cpp b/src/type_check.cpp index 98b821c..b6dddfc 100644 --- a/src/type_check.cpp +++ b/src/type_check.cpp @@ -3,6 +3,80 @@ #include "yaml-schema-cpp/yaml_schema.hpp" #include "yaml-schema-cpp/yaml_utils.hpp" +// Macros used by checkNodeAs*() to check a node depending on the type string +/* + * If type is TypeName, converts node to TypeName (it may throw) and returns true. + */ +#define CHECK_TYPE_CASE(TypeName) \ + if (type == #TypeName) \ + { \ + node.as(); \ + return true; \ + } + +#if _EIGEN_FOUND == 1 +/* + * If type is "[Eigen::]Vectord" or "[Eigen::]Matrixd", converts node to it (it may throw) and + * returns true. + */ +#define CHECK_TYPE_EIGEN_CASE(size) \ + if (type == "Vector" #size "d" or type == "Eigen::Vector" #size "d") \ + { \ + node.as >(); \ + return true; \ + } \ + if (type == "Matrix" #size "d" or type == "Eigen::Matrix" #size "d") \ + { \ + node.as >(); \ + return true; \ + } + +/* + * CHECK_TYPE_CASE for all Eigen types (fixed size from 1 to 10, and dynamic size). + */ +#define CHECK_TYPE_EIGEN_CASES \ + CHECK_TYPE_EIGEN_CASE(1) \ + CHECK_TYPE_EIGEN_CASE(2) \ + CHECK_TYPE_EIGEN_CASE(3) \ + CHECK_TYPE_EIGEN_CASE(4) \ + CHECK_TYPE_EIGEN_CASE(5) \ + CHECK_TYPE_EIGEN_CASE(6) \ + CHECK_TYPE_EIGEN_CASE(7) \ + CHECK_TYPE_EIGEN_CASE(8) \ + CHECK_TYPE_EIGEN_CASE(9) \ + CHECK_TYPE_EIGEN_CASE(10) \ + CHECK_TYPE_CASE(Eigen::VectorXd) \ + CHECK_TYPE_CASE(Eigen::MatrixXd) \ + { \ + using namespace Eigen; \ + CHECK_TYPE_CASE(VectorXd) \ + CHECK_TYPE_CASE(MatrixXd) \ + } +#endif + +/* + * CHECK_TYPE_CASE for all basic types. + */ +#define CHECK_TYPE_BASIC_CASES \ + CHECK_TYPE_CASE(bool) \ + CHECK_TYPE_CASE(char) \ + CHECK_TYPE_CASE(int) \ + CHECK_TYPE_CASE(unsigned int) \ + CHECK_TYPE_CASE(long int) \ + CHECK_TYPE_CASE(long unsigned int) \ + CHECK_TYPE_CASE(float) \ + CHECK_TYPE_CASE(double) + +/* + * CHECK_TYPE_CASE for all string types. + */ +#define CHECK_TYPE_STRING_CASES \ + CHECK_TYPE_CASE(std::string) \ + { \ + using std::string; \ + CHECK_TYPE_CASE(string) \ + } + namespace yaml_schema_cpp { @@ -33,12 +107,16 @@ bool checkNodeAsBasic(const YAML::Node& node, const std::string& type) // array type if (isArrayType(type)) { + // the lowest element type has to be a basic type + if (not isBasicType(getLowestElementType(type))) return false; + // call recursively checkNodeAsBasic for each element of sequence - for (auto i = 0; node.size(); i++) checkNodeAsBasic(node[i], getLowerElementType(type)); + for (size_t i = 0; i < node.size(); i++) + if (not checkNodeAsBasic(node[i], getLowerElementType(type))) return false; + return true; } // scalar - else - CHECK_TYPE_BASIC_CASES + CHECK_TYPE_BASIC_CASES return false; } @@ -49,12 +127,16 @@ bool checkNodeAsString(const YAML::Node& node, const std::string& type) // array type if (isArrayType(type)) { - // call recursively checkNodeAsBasic for each element of sequence - for (auto i = 0; node.size(); i++) checkNodeAsString(node[i], getLowerElementType(type)); + // the lowest element type has to be a string type + if (not isStringType(getLowestElementType(type))) return false; + + // call recursively checkNodeAsString for each element of sequence + for (size_t i = 0; i < node.size(); i++) + if (not checkNodeAsString(node[i], getLowerElementType(type))) return false; + return true; } // scalar - else - CHECK_TYPE_STRING_CASES + CHECK_TYPE_STRING_CASES return false; } @@ -66,11 +148,16 @@ bool checkNodeAsEigen(const YAML::Node& node, const std::string& type) // array type if (isArrayType(type)) { - // call recursively checkNodeAsBasic for each element of sequence - for (auto i = 0; node.size(); i++) checkNodeAsString(node[i], getLowerElementType(type)); + // the lowest element type has to be an Eigen type + if (not isEigenType(getLowestElementType(type))) return false; + + // call recursively checkNodeAsEigen for each element of sequence + for (size_t i = 0; i < node.size(); i++) + if (not checkNodeAsEigen(node[i], getLowerElementType(type))) return false; + return true; } - else - CHECK_TYPE_EIGEN_CASES + // scalar + CHECK_TYPE_EIGEN_CASES return false; } #endif @@ -82,17 +169,20 @@ bool checkNodeAs(const YAML::Node& node, const std::string& type) // array type if (isArrayType(type)) { - // call recursively checkNodeAsBasic for each element of sequence - for (auto i = 0; node.size(); i++) checkNodeAsString(node[i], getLowerElementType(type)); + // the lowest element type has to be a trivial type + if (not isTrivialType(getLowestElementType(type))) return false; + + // call recursively checkNodeAs for each element of sequence + for (size_t i = 0; i < node.size(); i++) + if (not checkNodeAs(node[i], getLowerElementType(type))) return false; + return true; } - else - { - CHECK_TYPE_BASIC_CASES - CHECK_TYPE_STRING_CASES + // scalar + CHECK_TYPE_BASIC_CASES + CHECK_TYPE_STRING_CASES #if _EIGEN_FOUND == 1 - CHECK_TYPE_EIGEN_CASES + CHECK_TYPE_EIGEN_CASES #endif - } return false; } @@ -109,8 +199,26 @@ bool tryNodeAs(const YAML::Node& node, const std::string& type) return false; } +// A type string with malformed array brackets (e.g. "int[", "int[x]") is not a known type +static bool isWellFormedType(const std::string& type) +{ + try + { + std::string lower_type = type; + while (isArrayType(lower_type)) lower_type = getLowerElementType(lower_type); + } + catch (const std::exception& e) + { + return false; + } + return true; +} + +// NOTE: is*Type() functions call checkNodeAs*() with a null node. If the type is known, either the conversion throws +// or succeeds (returning true). Otherwise, false is returned. bool isTrivialType(const std::string& type) { + if (not isWellFormedType(type)) return false; try { return checkNodeAs(YAML::Node(), type); @@ -124,6 +232,7 @@ bool isTrivialType(const std::string& type) bool isBasicType(const std::string& type) { + if (not isWellFormedType(type)) return false; try { return checkNodeAsBasic(YAML::Node(), type); @@ -137,6 +246,7 @@ bool isBasicType(const std::string& type) bool isStringType(const std::string& type) { + if (not isWellFormedType(type)) return false; try { return checkNodeAsString(YAML::Node(), type); @@ -151,6 +261,7 @@ bool isStringType(const std::string& type) #if _EIGEN_FOUND == 1 bool isEigenType(const std::string& type) { + if (not isWellFormedType(type)) return false; try { return checkNodeAsEigen(YAML::Node(), type); diff --git a/src/yaml_generator.cpp b/src/yaml_generator.cpp index 87de1b8..360e7ab 100644 --- a/src/yaml_generator.cpp +++ b/src/yaml_generator.cpp @@ -2,8 +2,10 @@ #include "yaml-schema-cpp/yaml_generator.hpp" +#include #include #include +#include #include "yaml-schema-cpp/filesystem_wrapper.hpp" #include "yaml-schema-cpp/yaml_conversion.hpp" @@ -14,14 +16,81 @@ namespace yaml_schema_cpp { -int N_SEQUENCE_OUTPUT = 2; +// number of elements generated for sequences of unspecified size +static const size_t N_SEQUENCE_OUTPUT = 2; + +namespace +{ +// Emits a key as YAML (quoted if necessary) +std::string emitKey(const YAML::Node& key) +{ + YAML::Emitter emitter; + emitter << key; + return emitter.c_str(); +} + +// Writes the template node as YAML text. The scalars are written as they are, since they already contain the value +// (as YAML text) followed by a comment (see schemaToYaml()). +void writeTemplate(std::ostream& os, const YAML::Node& node, const std::string& indent) +{ + if (node.IsMap()) + { + for (auto child : node) + { + os << indent << emitKey(child.first) << ":"; + if (child.second.IsScalar()) + os << " " << child.second.Scalar() << "\n"; + else + { + os << "\n"; + writeTemplate(os, child.second, indent + " "); + } + } + } + else if (node.IsSequence()) + { + for (auto child : node) + { + if (child.IsScalar()) + os << indent << "- " << child.Scalar() << "\n"; + else + { + os << indent << "-\n"; + writeTemplate(os, child, indent + " "); + } + } + } + else if (node.IsScalar()) + os << indent << node.Scalar() << "\n"; +} + +// Puts the string in double quotes, escaping '\' and '"' +std::string doubleQuoted(const std::string& str) +{ + std::string quoted = "\""; + for (auto c : str) + { + if (c == '\\' or c == '"') quoted += '\\'; + quoted += c; + } + return quoted + "\""; +} + +// Replaces new lines by spaces (to be used in comments), removing trailing new lines +std::string oneLine(std::string str) +{ + while (not str.empty() and (str.back() == '\n' or str.back() == '\r')) str.pop_back(); + std::replace(str.begin(), str.end(), '\n', ' '); + std::replace(str.begin(), str.end(), '\r', ' '); + return str; +} +} // namespace std::string generateTemplate(std::string filepath, const std::string& name_schema, const std::vector& folders_schema, bool override) { - std::cout << "generateTemplate " << name_schema << std::endl; // Check extension if (filesystem::path(filepath).extension().empty()) { @@ -53,16 +122,12 @@ std::string generateTemplate(std::string filepath, } // Create Template Node - auto template_node = generateYaml(name_schema, folders_schema); + auto template_node = generateYaml(name_schema, folders_schema, override); - // Modify text + // Template text std::stringstream template_ss; - template_ss << template_node; - // remove " " + writeTemplate(template_ss, template_node, ""); std::string template_string = template_ss.str(); - template_string.erase(std::remove(template_string.begin(), template_string.end(), '"'), template_string.end()); - // change ' ' by " " - std::replace(template_string.begin(), template_string.end(), '\'', '"'); // Create file std::ofstream template_file; @@ -80,8 +145,6 @@ std::string generateTemplate(std::string filepath, YAML::Node generateYaml(std::string name_schema, const std::vector& folders_schema, bool override) { - std::cout << "generateYaml " << name_schema << std::endl; - // Find schema file + check extension std::stringstream log; auto path_schema = findSchema(name_schema, folders_schema, log); @@ -120,8 +183,7 @@ YAML::Node generateYaml(std::string name_schema, const std::vector& } catch (const std::exception& e) { - throw std::runtime_error("In loadSchema: The schema file " + path_schema + - " is not valid. Error: " + e.what()); + throw std::runtime_error("generateYaml: The schema file " + path_schema + " is not valid. Error: " + e.what()); } // Generate YAML from schema @@ -137,7 +199,12 @@ void schemaToYaml(const YAML::Node& node_schema, const std::vector& folders_schema, bool override) { - std::cout << "schemaToYaml:\n" << node_schema << std::endl; + // scalar schema nodes are ignored (as in checkSchema()) + if (node_schema.IsScalar()) + { + node_output = YAML::Null; + return; + } // Specification (has _mandatory, _type and _doc) if (isSpecification(node_schema)) @@ -183,13 +250,14 @@ void schemaToYaml(const YAML::Node& node_schema, else value_str = getZeroString(node_schema[TYPE].as()); - // put ' ' envolving the value if type string - if (isStringType(node_schema[TYPE].as())) value_str = "'" + value_str + "'"; + // put " " envolving the value if type string + if (isStringType(node_schema[TYPE].as())) value_str = doubleQuoted(value_str); - // FILL NODE - node_output = value_str + " # " + mandatory_str + "DOC " + node_schema[DOC].as() + - " - TYPE " + node_schema[TYPE].as() + - (node_schema[OPTIONS] ? " - OPTIONS " + sequenceToString(node_schema[OPTIONS]) : ""); + // FILL NODE: value (as YAML text) followed by a comment + node_output = + value_str + " # " + mandatory_str + "DOC " + oneLine(node_schema[DOC].as()) + + " - TYPE " + node_schema[TYPE].as() + + (node_schema[OPTIONS] ? " - OPTIONS " + oneLine(sequenceToString(node_schema[OPTIONS])) : ""); } // custom non trivial-type or Derived type else @@ -201,7 +269,7 @@ void schemaToYaml(const YAML::Node& node_schema, if (lowest_type == "derived") { node_output["type"] = - "'DerivedType' # DOC String corresponding to the name of the object class (and its schema " + "\"DerivedType\" # DOC String corresponding to the name of the object class (and its schema " "file). Should be a class derived from base class: " + node_schema[BASE].as(); node_output["follow"] = "some/path/to/derived/type/parameters.yaml"; @@ -224,7 +292,7 @@ void schemaToYaml(const YAML::Node& node_schema, if (seq_size == 0) seq_size = N_SEQUENCE_OUTPUT; node_schema_i[TYPE] = getLowerElementType(node_schema[TYPE].as()); - for (auto i = 0; i < seq_size; i++) + for (size_t i = 0; i < seq_size; i++) { // if VALUE --> copy corresponding if (node_schema[VALUE]) node_schema_i[VALUE] = node_schema[VALUE][i]; diff --git a/src/yaml_schema.cpp b/src/yaml_schema.cpp index 9812dba..7a51e97 100644 --- a/src/yaml_schema.cpp +++ b/src/yaml_schema.cpp @@ -133,13 +133,13 @@ void checkSchema(const YAML::Node& node_schema, } // check value (optional) - if (node_schema[VALUE]) checkSchemaValue(node_schema, node_field, node_schema_parent, folders_schema); + if (node_schema[VALUE]) checkSchemaValue(node_schema, node_field, folders_schema); // check default (optional) - if (node_schema[DEFAULT]) checkSchemaDefault(node_schema, node_field, node_schema_parent, folders_schema); + if (node_schema[DEFAULT]) checkSchemaDefault(node_schema, node_field, folders_schema); // check options (optional) - if (node_schema[OPTIONS]) checkSchemaOptions(node_schema, node_field, node_schema_parent, folders_schema); + if (node_schema[OPTIONS]) checkSchemaOptions(node_schema, node_field, folders_schema); } // no specifications else @@ -166,7 +166,6 @@ void checkSchema(const YAML::Node& node_schema, void checkSchemaValue(const YAML::Node& node_schema, const std::string& node_field, - const YAML::Node& node_schema_parent, const std::vector& folders_schema) { // Check VALUE follows the corresponding schema @@ -190,7 +189,6 @@ void checkSchemaValue(const YAML::Node& node_schema, void checkSchemaDefault(const YAML::Node& node_schema, const std::string& node_field, - const YAML::Node& node_schema_parent, const std::vector& folders_schema) { // Check not mandatory @@ -213,7 +211,6 @@ void checkSchemaDefault(const YAML::Node& node_schema, void checkSchemaOptions(const YAML::Node& node_schema, const std::string& node_field, - const YAML::Node& node_schema_parent, const std::vector& folders_schema) { // Check that it is a sequence @@ -224,14 +221,14 @@ void checkSchemaOptions(const YAML::Node& node_schema, // Check options against corresponding schema (if "derived" --> BASE) std::string type = getCheckType(node_schema); // If derived type take BASE - for (auto n_i = 0; n_i < node_schema[OPTIONS].size(); n_i++) + for (size_t n_i = 0; n_i < node_schema[OPTIONS].size(); n_i++) { std::stringstream log; YAML::Node node_schema_option_i = node_schema[OPTIONS][n_i]; if (not applySchema(node_schema_option_i, type, folders_schema, log, "")) { - throw std::runtime_error("YAML schema: " + node_field + ", " + DEFAULT + - " did not pass the schema check with error: " + log.str()); + throw std::runtime_error("YAML schema: " + node_field + ", " + OPTIONS + "[" + std::to_string(n_i) + + "] did not pass the schema check with error: " + log.str()); } } @@ -338,25 +335,28 @@ bool applySchema(YAML::Node& node_input, size_t size; if (isArrayType(type, size)) { - bool is_valid = true; // If node not sequence complain if (not node_input.IsSequence()) { - writeErrorToLog(log, acc_field, YAML::Node(YAML::NodeType::Undefined), " should be a sequence: " + type); - is_valid = false; + writeErrorToLog( + log, acc_field, YAML::Node(YAML::NodeType::Undefined), "Wrong type, should be a sequence: " + type); + return false; } + + bool is_valid = true; + // If size defined in type (!=0), complain if different if (size != 0 and node_input.size() != size) { writeErrorToLog(log, acc_field, YAML::Node(YAML::NodeType::Undefined), - " wrong size, should be " + std::to_string(size)); + "Wrong size, should be " + std::to_string(size)); is_valid = false; } // applySchema recursively for all nodes in sequence - for (auto i = 0; i < node_input.size(); i++) + for (size_t i = 0; i < node_input.size(); i++) { YAML::Node node_input_i = node_input[i]; is_valid = applySchema(node_input_i, @@ -375,6 +375,15 @@ bool applySchema(YAML::Node& node_input, // If trivial type (no schema file) if (isTrivialType(type)) { + // null is not a valid string (yaml-cpp would convert it to "null") + if (node_input.IsDefined() and node_input.IsNull() and isStringType(type)) + { + writeErrorToLog(log, + acc_field, + YAML::Node(YAML::NodeType::Undefined), + "Wrong type, should be " + type + " (found null value)"); + return false; + } if (not tryNodeAs(node_input, type)) { writeErrorToLog( @@ -402,87 +411,85 @@ bool applySchemaRecursive(YAML::Node& node_input, const std::vector& folders, std::stringstream& log, const std::string& acc_field, - bool override) + bool override, + const YAML::Node& node_schema_parent) { + // scalar schema nodes are ignored (as in checkSchema()) + if (node_schema.IsScalar()) return true; + bool is_valid = true; // Param schema (has mandatory and type) if (isSpecification(node_schema)) { - // If exists, check VALUE, derived->BASE & OPTIONS + // If exists, check type, VALUE & OPTIONS if (node_input.IsDefined()) { - // If VALUE defined in schema, complain if different - if (node_schema[VALUE] and - not compare(node_schema[VALUE], node_input, node_schema[TYPE].as(), folders)) - { - writeErrorToLog(log, - acc_field, - node_schema, - " already defined in schema with a different value. Not allowed to be changed."); - is_valid = false; - } - - // Derived type ( "derived" or "derived[]" or "derived[][]".. ) + // check type: + bool type_valid; + // - derived type ( "derived" or "derived[]" or "derived[][]".. ) if (isDerivedType(node_schema[TYPE].as())) - { - is_valid = applySchemaDerived( - node_input, node_input_parent, node_schema, folders, log, acc_field, override) and - is_valid; - } - // Type specified (either trivial or custom) + type_valid = + applySchemaDerived(node_input, node_input_parent, node_schema, folders, log, acc_field, override); + // - type specified (either trivial or custom), check with corresponding schema file or trivial type else + type_valid = + applySchema(node_input, node_schema[TYPE].as(), folders, log, acc_field, override); + + is_valid = type_valid; + + // check VALUE and OPTIONS (only if passed type validation) + if (type_valid) { - // check with corresponding schema file or trivial type - if (not applySchema( - node_input, node_schema[TYPE].as(), folders, log, acc_field, override)) + // type to be compared with VALUE and OPTIONS (if derived type, BASE) + std::string check_type = getCheckType(node_schema); + + // If VALUE defined in schema, complain if different + if (node_schema[VALUE] and not compare(node_schema[VALUE], node_input, check_type, folders)) { + writeErrorToLog(log, + acc_field, + node_schema, + "Already defined in schema with a different value. Not allowed to be changed."); is_valid = false; } - // check if value is in OPTIONS (only if passed schema validation) - else if (node_schema[OPTIONS]) + + // check if value is in OPTIONS + if (node_schema[OPTIONS] and not isInOptions(node_input, node_schema[OPTIONS], check_type, folders)) { - if (not isInOptions( - node_input, node_schema[OPTIONS], node_schema[TYPE].as(), folders)) - { - writeErrorToLog( - log, acc_field, node_schema, "Wrong value. Allowed values defined in OPTIONS."); - is_valid = false; - } + writeErrorToLog(log, acc_field, node_schema, "Wrong value. Allowed values defined in OPTIONS."); + is_valid = false; } } } - // Does not exist -> check if MANDATORY (add DEFAULT) or add VALUE + // Does not exist -> add VALUE or check if MANDATORY (or add DEFAULT) + // NOTE: node_input was obtained via node_input_parent[key], assigning it adds the key to node_input_parent else { // Load VALUE in case defined in schema if (node_schema[VALUE]) { - // add node with value (if parent is defined) - if (node_input_parent.IsDefined()) - { - auto field = filesystem::path(acc_field).filename().string(); - node_input_parent[field] = Clone(node_schema[VALUE]); - } + node_input = Clone(node_schema[VALUE]); } // Check if it is mandatory else { - bool mandatory; + bool mandatory = false; if (isExpression(node_schema[MANDATORY])) { try { - mandatory = evalExpression(node_schema[MANDATORY].as(), node_input_parent); + mandatory = evalExpression( + node_schema[MANDATORY].as(), node_input_parent, node_schema_parent); } catch (const std::exception& e) { - writeErrorToLog(log, - acc_field, - node_schema, - "Evaluating schema expression for 'mandatory' of field " + acc_field + - " failed with error: " + e.what() + "\n"); - is_valid = false; + writeErrorToLog( + log, + acc_field, + node_schema, + "Evaluating schema expression for '" + MANDATORY + "' failed with error: " + e.what()); + return false; } } else @@ -494,19 +501,14 @@ bool applySchemaRecursive(YAML::Node& node_input, writeErrorToLog(log, acc_field, node_schema, - "Missing mandatory field (" + MANDATORY + - "): " + node_schema[MANDATORY].as() + ")."); + "Missing mandatory field (" + MANDATORY + ": " + + node_schema[MANDATORY].as() + ")."); is_valid = false; } - // add node with default value (if parent is defined) + // add node with default value else if (node_schema[DEFAULT]) { - if (not node_input_parent.IsDefined()) - { - throw std::runtime_error("node_input_parent not defined"); - } - auto field = filesystem::path(acc_field).filename().string(); - node_input_parent[field] = Clone(node_schema[DEFAULT]); + node_input = Clone(node_schema[DEFAULT]); } } } @@ -514,28 +516,41 @@ bool applySchemaRecursive(YAML::Node& node_input, // map without specification else { - // if doesn't exist, we create it. If it should have mandatory fields, it will crash later. + // if doesn't exist, we create it. If it should have mandatory fields, it will fail later. if (not node_input.IsDefined()) { - node_input = YAML::Node(); - auto field = filesystem::path(acc_field).filename().string(); - node_input_parent[field] = node_input; + node_input = YAML::Node(); } - - // iterate all childs - for (auto node_schema_child : node_schema) + // if exists, it should be a map (null is accepted and converted to map) + else if (node_schema.IsMap() and not node_input.IsMap() and not node_input.IsNull()) { - YAML::Node node_input_child = node_input[node_schema_child.first.as()]; + writeErrorToLog(log, acc_field, YAML::Node(YAML::NodeType::Undefined), "Wrong type, should be a map."); + return false; + } - is_valid = applySchemaRecursive( - node_input_child, - node_input, - node_schema_child.second, - folders, - log, - (acc_field.empty() ? "" : acc_field + "/") + node_schema_child.first.as(), - override) and - is_valid; + // iterate all childs in two passes: first the ones without expression in MANDATORY, so that their DEFAULT + // or VALUE are already added when evaluating the expressions of the rest (that may depend on them) + for (auto expression_pass : {false, true}) + { + for (auto node_schema_child : node_schema) + { + bool has_expression = + isSpecification(node_schema_child.second) and isExpression(node_schema_child.second[MANDATORY]); + if (has_expression != expression_pass) continue; + + YAML::Node node_input_child = node_input[node_schema_child.first.as()]; + + is_valid = applySchemaRecursive( + node_input_child, + node_input, + node_schema_child.second, + folders, + log, + (acc_field.empty() ? "" : acc_field + "/") + node_schema_child.first.as(), + override, + node_schema) and + is_valid; + } } } @@ -559,7 +574,7 @@ bool applySchemaDerived(YAML::Node& node_input, // If node not sequence complain if (not node_input.IsSequence()) { - writeErrorToLog(log, acc_field, node_schema, "Should be a sequence"); + writeErrorToLog(log, acc_field, node_schema, "Wrong type, should be a sequence."); return false; } // If size defined in type (!=0), complain if different @@ -568,13 +583,13 @@ bool applySchemaDerived(YAML::Node& node_input, writeErrorToLog(log, acc_field, YAML::Node(YAML::NodeType::Undefined), - " wrong size, should be " + std::to_string(size)); + "Wrong size, should be " + std::to_string(size)); is_valid = false; } // applySchemaDerived recursively for all nodes in sequence YAML::Node node_schema_i = YAML::Clone(node_schema); node_schema_i[TYPE] = getLowerElementType(node_schema[TYPE].as()); - for (auto i = 0; i < node_input.size(); i++) + for (size_t i = 0; i < node_input.size(); i++) { YAML::Node node_input_i = node_input[i]; is_valid = applySchemaDerived(node_input_i, @@ -590,11 +605,18 @@ bool applySchemaDerived(YAML::Node& node_input, } else { - // check existence of key type - if (not node_input["type"]) + // check it is a map containing the key 'type' of type string + if (not node_input.IsMap() or not node_input["type"]) { - writeErrorToLog( - log, acc_field, node_schema, "Does not contain key 'type' which is mandatory for 'derived'."); + writeErrorToLog(log, + acc_field, + node_schema, + "Should be a map containing the key 'type', which is mandatory for 'derived'."); + return false; + } + if (not tryNodeAs(node_input["type"], "string")) + { + writeErrorToLog(log, acc_field, node_schema, "The key 'type' should be a string."); return false; } @@ -633,7 +655,8 @@ void addNodeSchema(YAML::Node& node, { if (node[key]) { - if (isSpecification(node[key])) + // existing specification or leaf (scalar or sequence) --> override (or throw) + if (isSpecification(node[key]) or node[key].IsScalar() or node[key].IsSequence()) { if (override) { @@ -644,6 +667,7 @@ void addNodeSchema(YAML::Node& node, throw std::runtime_error("Trying to add something into an already existing schema node"); } } + // existing map (or null) --> add all children of value else { if (hasAnyReservedKey(node[key])) @@ -655,7 +679,8 @@ void addNodeSchema(YAML::Node& node, for (auto value_map_node : value) { YAML::Node node_key = node[key]; - addNodeSchema(node_key, value_map_node.first.as(), value_map_node.second, override); + addNodeSchema( + node_key, value_map_node.first.as(), value_map_node.second, override, parent_path); } } } @@ -665,20 +690,21 @@ void addNodeSchema(YAML::Node& node, } // relative path input case - if (node[key].Type() == YAML::NodeType::Scalar) + if (not parent_path.empty() and node[key].Type() == YAML::NodeType::Scalar) { std::string value_str = node[key].as(); - if ((value_str.size() > 1 and value_str.substr(0, 2) == "./") or - (value_str.size() > 2 and value_str.substr(0, 3) == "../")) + if (isRelativePath(value_str)) { - filesystem::path path_value = filesystem::path(parent_path) / filesystem::path(value.as()); - node[key] = path_value.string(); + node[key] = (filesystem::path(parent_path) / filesystem::path(value_str)).string(); } } } bool isSpecification(const YAML::Node& node_schema) { + // only maps can be specifications + if (not node_schema.IsDefined() or not node_schema.IsMap()) return false; + for (auto required_key : REQUIRED_KEYS) { if (not node_schema[required_key]) @@ -691,6 +717,9 @@ bool isSpecification(const YAML::Node& node_schema) bool hasAnyReservedKey(const YAML::Node& node_schema) { + // only maps can have keys + if (not node_schema.IsDefined() or not node_schema.IsMap()) return false; + for (auto reserved_key : RESERVED_KEYS) { if (node_schema[reserved_key]) diff --git a/src/yaml_server.cpp b/src/yaml_server.cpp index bb83cd5..5a349ed 100644 --- a/src/yaml_server.cpp +++ b/src/yaml_server.cpp @@ -21,6 +21,11 @@ YamlServer::YamlServer(const std::vector& folders_schema, const std loadYaml(path_input); } +YamlServer::YamlServer(const std::vector& folders_schema, const char* path_input, bool override) + : YamlServer(folders_schema, std::string(path_input), override) +{ +} + void YamlServer::addFolderSchema(const std::vector& folders_schema, bool before) { folders_schema_.insert( @@ -55,6 +60,7 @@ void YamlServer::loadYaml(const std::string& path_input) void YamlServer::setYaml(const YAML::Node _node_input) { + path_input_.clear(); node_input_ = Clone(_node_input); } diff --git a/src/yaml_template_generator.cpp b/src/yaml_template_generator.cpp index 71182a3..5e6dafc 100644 --- a/src/yaml_template_generator.cpp +++ b/src/yaml_template_generator.cpp @@ -26,9 +26,11 @@ int main(int argc, char* argv[]) * NOTE 2: 'output_file' will be modified to avoid overriding existing files. */ - // HELP - if (argc == 2 and (std::string(argv[1]) == "-h" or std::string(argv[1]) == "--help")) + // HELP (also if wrong number of arguments) + bool help = argc == 2 and (std::string(argv[1]) == "-h" or std::string(argv[1]) == "--help"); + if (help or (argc != 3 and argc != 4)) { + if (not help) cout << red << "ERROR: Wrong number of arguments." << reset << endl << endl; cout << "--------- yaml_template_generator HELP ---------- \nCall it with:" << endl; cout << "yaml_template_generator schema_name schema_folders output_file" << endl << endl; cout << "'schema_name': Schema to be applied (just its name, without path or extension)" << endl; @@ -41,10 +43,9 @@ int main(int argc, char* argv[]) cout << " name based on schema_name (requires `$HOME` to be defined)." << endl << endl; cout << "NOTE 1: Paths can be absolute (starting by '/') or relative." << endl; cout << "NOTE 2: 'output_file' will be modified to avoid overriding existing files." << endl; - return 0; + return help ? 0 : 1; } // Call - if (argc == 3 or argc == 4) { filesystem::path current_path(filesystem::current_path()); @@ -54,7 +55,8 @@ int main(int argc, char* argv[]) // schema folders std::string schema_folders_input(argv[2]); std::vector schema_folders; - if (schema_folders_input.front() == '[' and schema_folders_input.back() == ']') + if (schema_folders_input.size() > 1 and schema_folders_input.front() == '[' and + schema_folders_input.back() == ']') { // remove '[' and ']' schema_folders_input.erase(0, 1); @@ -63,31 +65,42 @@ int main(int argc, char* argv[]) size_t pos = 0; while (not schema_folders_input.empty()) { + // remove spaces + while (not schema_folders_input.empty() and schema_folders_input.front() == ' ') + schema_folders_input.erase(0, 1); + if (schema_folders_input.empty()) break; + pos = schema_folders_input.find(" "); schema_folders.push_back(schema_folders_input.substr(0, pos)); // next folder schema_folders_input.erase(0, pos); - - // remove spaces - while (schema_folders_input.front() == ' ') schema_folders_input.erase(0, 1); } } - else + else if (not schema_folders_input.empty()) schema_folders.push_back(schema_folders_input); + if (schema_folders.empty()) + { + cout << red << "ERROR: No schema folders provided." << reset << endl; + return 1; + } + // Correct paths to absolute - for (auto i = 0; i < schema_folders.size(); i++) + for (size_t i = 0; i < schema_folders.size(); i++) { // HOME char '~' if (schema_folders[i].front() == '~') + { if (getenv("HOME") == NULL) { - cout << red << "ERROR: path contains '~' but environment variable HOME not defined." << reset; + cout << red << "ERROR: path contains '~' but environment variable HOME not defined." << reset + << endl; return 1; } else schema_folders[i] = std::string(getenv("HOME")) + schema_folders[i].substr(1); + } // relative path if (schema_folders[i].front() != '/') schema_folders[i] = current_path.string() + "/" + schema_folders[i]; @@ -101,7 +114,7 @@ int main(int argc, char* argv[]) { cout << red << "ERROR: Environment variable HOME not defined (mandatory if 'output_file' not provided)." - << reset; + << reset << endl; return 1; } output_file = std::string(getenv("HOME")) + "/" + schema + "_template.yaml"; diff --git a/src/yaml_utils.cpp b/src/yaml_utils.cpp index f181483..e0b380e 100644 --- a/src/yaml_utils.cpp +++ b/src/yaml_utils.cpp @@ -1,5 +1,6 @@ #include "yaml-schema-cpp/yaml_utils.hpp" +#include #include #include #include @@ -8,9 +9,112 @@ #include "yaml-schema-cpp/type_check.hpp" #include "yaml-schema-cpp/filesystem_wrapper.hpp" #include "yaml-schema-cpp/yaml_schema.hpp" +#include "yaml-schema-cpp/expression.hpp" + +// Macros used by compareTrivial() to compare two nodes depending on the type string +/* + * Used in compareTrivial(): if type is TypeName, returns if node1 and node2 are equal as TypeName (false if + * they cannot be converted). + */ +#define COMPARE_TYPE(TypeName) \ + if (type == #TypeName) \ + { \ + try \ + { \ + return node1.as() == node2.as(); \ + } \ + catch (const std::exception& e) \ + { \ + return false; \ + } \ + } + +#if _EIGEN_FOUND == 1 +/* + * Used in compareTrivial(): if type is "[Eigen::]Vectord" or "[Eigen::]Matrixd", returns if node1 + * and node2 are equal (tolerance 1e-9) as that type (false if they cannot be converted). + */ +#define COMPARE_EIGEN(size) \ + if (type == "Vector" #size "d" or type == "Eigen::Vector" #size "d") \ + { \ + try \ + { \ + return (node1.as >() - node2.as >()) \ + .cwiseAbs() \ + .maxCoeff() < 1e-9; \ + } \ + catch (const std::exception& e) \ + { \ + return false; \ + } \ + } \ + if (type == "Matrix" #size "d" or type == "Eigen::Matrix" #size "d") \ + { \ + try \ + { \ + return (node1.as >() - node2.as >()) \ + .cwiseAbs() \ + .maxCoeff() < 1e-9; \ + } \ + catch (const std::exception& e) \ + { \ + return false; \ + } \ + } + +/* + * Used in compareTrivial(): if type is TypeName (dynamic size Eigen type), returns if node1 and node2 have the + * same size and are equal (tolerance 1e-9) as TypeName (false if they cannot be converted). + */ +#define COMPARE_EIGEN_DYNAMIC(TypeName) \ + if (type == #TypeName) \ + { \ + try \ + { \ + TypeName m1 = node1.as(); \ + TypeName m2 = node2.as(); \ + return m1.rows() == m2.rows() and m1.cols() == m2.cols() and \ + (m1.size() == 0 or (m1 - m2).cwiseAbs().maxCoeff() < 1e-9); \ + } \ + catch (const std::exception& e) \ + { \ + return false; \ + } \ + } +#endif namespace yaml_schema_cpp { +namespace +{ +// Canonical paths of the files currently being followed (and flattened), to detect 'follow' cycles +thread_local std::vector follow_stack; + +// Adds a followed file to follow_stack during its lifetime. Throws if the file is already there ('follow' cycle). +struct FollowGuard +{ + FollowGuard(const std::string& path_follow) + { + std::string canonical_path = filesystem::canonical(path_follow).string(); + + auto it = std::find(follow_stack.begin(), follow_stack.end(), canonical_path); + if (it != follow_stack.end()) + { + std::string cycle; + for (; it != follow_stack.end(); it++) cycle += filesystem::path(*it).filename().string() + " -> "; + cycle += filesystem::path(canonical_path).filename().string(); + throw std::runtime_error("In flattenNode: 'follow' cycle detected: " + cycle); + } + follow_stack.push_back(canonical_path); + } + ~FollowGuard() + { + follow_stack.pop_back(); + } + FollowGuard(const FollowGuard&) = delete; + FollowGuard& operator=(const FollowGuard&) = delete; +}; +} // namespace void flattenNode(YAML::Node& node, std::string current_folder, @@ -71,7 +175,13 @@ void flattenMap(YAML::Node& node, // Case input yaml else { - addNodeYaml(node_aux, n.first.as(), n.second, override, current_folder); + // Relative paths are resolved only for the scalars of this file (map and sequence children were + // already resolved by flattenNode() above) + addNodeYaml(node_aux, + n.first.as(), + n.second, + override, + n.second.IsScalar() ? current_folder : ""); } } } @@ -105,19 +215,18 @@ void insertNodes(YAML::Node& node, // is schema? bool following_is_schema = filesystem::path(path_follow_str).extension() == SCHEMA_EXTENSION; - // following file is schema --> findFileRecursive + // following file is schema --> findFileRecursive (throws if not found) if (following_is_schema) { path_follow = findFileRecursive(path_follow_str, schema_folders); - if (path_follow.empty()) - { - throw std::runtime_error("In flattenNode: file '" + path_follow_str + "' not found"); - } } - // following file is regular yaml --> relative path - else if (filesystem::path(path_follow_str).extension() == ".yaml") + // following file is regular yaml --> absolute path or relative path to current_folder + else if (filesystem::path(path_follow_str).extension() == YAML_EXTENSION) { - path_follow = current_folder + "/" + path_follow_str; + // NOTE: absolute paths checked explicitly (boost::filesystem appends absolute paths in operator/) + filesystem::path follow_path(path_follow_str); + path_follow = + (follow_path.is_absolute() ? follow_path : filesystem::path(current_folder) / follow_path).string(); } // wrong extension else @@ -131,6 +240,9 @@ void insertNodes(YAML::Node& node, throw std::runtime_error("In flattenNode: the file '" + path_follow + "' does not exists"); } + // detect 'follow' cycles (path_follow is in follow_stack while it is loaded and flattened) + FollowGuard follow_guard(path_follow); + // load "following" file YAML::Node node_child = YAML::LoadFile(path_follow); @@ -146,25 +258,18 @@ void insertNodes(YAML::Node& node, } // add all new children to original node + // NOTE: relative paths of node_child were already resolved by flattenNode() above for (auto nc : node_child) { // Case schema if (following_is_schema) { - addNodeSchema(node, - nc.first.as(), - nc.second, - override, - filesystem::path(path_follow).parent_path().string()); + addNodeSchema(node, nc.first.as(), nc.second, override); } // Case input yaml else { - addNodeYaml(node, - nc.first.as(), - nc.second, - override, - filesystem::path(path_follow).parent_path().string()); + addNodeYaml(node, nc.first.as(), nc.second, override); } } } @@ -189,6 +294,7 @@ void addNodeYaml(YAML::Node& node, { throw std::runtime_error("Trying to add an already existing scalar node."); } + break; } case YAML::NodeType::Sequence: { for (auto value_seq_node : value) @@ -220,33 +326,39 @@ void addNodeYaml(YAML::Node& node, } // relative path input case - if (node[key].Type() == YAML::NodeType::Scalar) + if (not parent_path.empty() and node[key].Type() == YAML::NodeType::Scalar) { std::string value_str = node[key].as(); - if ((value_str.size() > 1 and value_str.substr(0, 2) == "./") or - (value_str.size() > 2 and value_str.substr(0, 3) == "../")) + if (isRelativePath(value_str)) { - filesystem::path path_value = filesystem::path(parent_path) / filesystem::path(value.as()); - node[key] = path_value.string(); + node[key] = (filesystem::path(parent_path) / filesystem::path(value_str)).string(); } } } +bool isRelativePath(const std::string& value) +{ + return (value.size() > 1 and value.substr(0, 2) == "./") or (value.size() > 2 and value.substr(0, 3) == "../"); +} + std::string findFileRecursive(const std::string& name_with_extension, const std::vector& folders) { std::string folders_str = ""; - std::string debug_str = ""; for (auto folder : folders) { - folders_str += folder + (filesystem::exists(folder) ? "" : "-NOT FOUND") + - (filesystem::is_directory(folder) ? "" : " -NOT A DIRECTORY") + ", "; - if (filesystem::exists(folder) and filesystem::is_directory(folder)) + if (not filesystem::exists(folder)) + folders_str += folder + " -NOT FOUND, "; + else if (not filesystem::is_directory(folder)) + folders_str += folder + " -NOT A DIRECTORY, "; + else { + folders_str += folder + ", "; + std::string last_entry; try { for (auto const& entry : filesystem::recursive_directory_iterator(folder)) { - debug_str += entry.path().string() + ", "; + last_entry = entry.path().string(); if (filesystem::is_regular_file(entry) and entry.path().filename().string() == name_with_extension) { return entry.path().string(); @@ -255,7 +367,8 @@ std::string findFileRecursive(const std::string& name_with_extension, const std: } catch (const std::exception& e) { - throw std::runtime_error("error: '" + std::string(e.what()) + "' iterated entries: " + debug_str); + throw std::runtime_error("findFileRecursive: error iterating folder '" + folder + "' (last entry: '" + + last_entry + "'): " + e.what()); } } } @@ -296,9 +409,14 @@ std::vector getAllSchemas(const std::vector& root_fold { std::vector schemas_found; for (auto root_folder : root_folders) + { + // skip folders that do not exist (as findFileRecursive() and validateAllSchemas() do) + if (not filesystem::exists(root_folder) or not filesystem::is_directory(root_folder)) continue; + for (auto const& entry : filesystem::recursive_directory_iterator(root_folder)) if (filesystem::is_regular_file(entry) and entry.path().extension() == SCHEMA_EXTENSION) schemas_found.push_back(entry.path().filename().string()); + } return schemas_found; } @@ -354,7 +472,7 @@ std::list findNodesWithKey(const YAML::Node root_node, const std::st if (root_node.IsSequence()) { - for (auto i = 0; i < root_node.size(); i++) + for (size_t i = 0; i < root_node.size(); i++) { auto nodes_with_key_i = findNodesWithKey(root_node[i], key); nodes_with_key.insert(nodes_with_key.end(), nodes_with_key_i.begin(), nodes_with_key_i.end()); @@ -381,7 +499,7 @@ std::string sequenceToString(const YAML::Node& node) if (not node.IsSequence()) throw std::runtime_error("sequenceToString is only for sequence nodes"); std::string ret("["); - for (auto i = 0; i < node.size(); i++) + for (size_t i = 0; i < node.size(); i++) { if (i > 0) ret += ", "; @@ -411,12 +529,21 @@ bool isArrayType(const std::string& type_str, size_t& size) // not sequence string ('[' not found) if (pos_open == std::string::npos) return false; + if (pos_close < pos_open) + throw std::runtime_error("isArrayType: type_str (" + type_str + ") contains ']' before '['."); + // array // 0: size not specified if (pos_open + 1 == pos_close) size = 0; // Return specified size else - size = std::stoi(type_str.substr(pos_open + 1, pos_close - pos_open - 1)); + { + std::string size_str = type_str.substr(pos_open + 1, pos_close - pos_open - 1); + if (size_str.find_first_not_of("0123456789") != std::string::npos) + throw std::runtime_error("isArrayType: type_str (" + type_str + + ") array size should be a positive integer or empty."); + size = std::stoul(size_str); + } return true; } @@ -475,8 +602,15 @@ std::string getCheckType(const YAML::Node& node) bool compareNodesAutoType(const YAML::Node& node1, const YAML::Node& node2) { + // Both undefined --> equal + if (node1.IsDefined() != node2.IsDefined()) return false; + if (not node1.IsDefined()) return true; + + // Both null --> equal + if (node1.IsNull() != node2.IsNull()) return false; + if (node1.IsNull()) return true; + // Different types - if (node1.IsDefined() != node2.IsDefined() or node1.IsNull() != node2.IsNull()) return false; if (node1.IsScalar() != node2.IsScalar()) return false; if (node1.IsSequence() != node2.IsSequence()) return false; if (node1.IsMap() != node2.IsMap()) return false; @@ -496,7 +630,7 @@ bool compareNodesAutoType(const YAML::Node& node1, const YAML::Node& node2) { if (node1.size() != node2.size()) return false; - for (auto i = 0; i < node1.size(); i++) + for (size_t i = 0; i < node1.size(); i++) if (not compareNodesAutoType(node1[i], node2[i])) return false; // all equal @@ -541,7 +675,7 @@ bool compare(const YAML::Node& node1, if (node1.size() != node2.size()) return false; // compare all elements - for (auto i = 0; i < node1.size(); i++) + for (size_t i = 0; i < node1.size(); i++) { if (not compare(node1[i], node2[i], getLowerElementType(type), folders_schema)) return false; } @@ -575,8 +709,8 @@ bool compareTrivial(const YAML::Node& node1, const YAML::Node& node2, const std: COMPARE_TYPE(string) } #if _EIGEN_FOUND == 1 - COMPARE_TYPE(Eigen::VectorXd) - COMPARE_TYPE(Eigen::MatrixXd) + COMPARE_EIGEN_DYNAMIC(Eigen::VectorXd) + COMPARE_EIGEN_DYNAMIC(Eigen::MatrixXd) COMPARE_EIGEN(1) COMPARE_EIGEN(2) @@ -590,8 +724,8 @@ bool compareTrivial(const YAML::Node& node1, const YAML::Node& node2, const std: COMPARE_EIGEN(10) { using namespace Eigen; - COMPARE_TYPE(VectorXd) - COMPARE_TYPE(VectorXd) + COMPARE_EIGEN_DYNAMIC(VectorXd) + COMPARE_EIGEN_DYNAMIC(MatrixXd) } #endif @@ -610,11 +744,17 @@ bool compareNonTrivialSchema(const YAML::Node& node1, if (node1.IsDefined() != node2.IsDefined()) return false; // Compare if MANDATORY or if both node1 and node2 are defined - if (node_schema[MANDATORY].as() or (node1.IsDefined() and node2.IsDefined())) + // (expression MANDATORY cannot be evaluated here, considered as optional) + bool mandatory = not isExpression(node_schema[MANDATORY]) and node_schema[MANDATORY].as(); + if (mandatory or (node1.IsDefined() and node2.IsDefined())) return compare(node1, node2, node_schema[TYPE].as(), folders_schema); } - else + // scalar schema nodes are ignored + else if (node_schema.IsMap()) { + // a scalar cannot be compared with a map schema + if ((node1.IsDefined() and node1.IsScalar()) or (node2.IsDefined() and node2.IsScalar())) return false; + for (auto node_schema_child : node_schema) { @@ -640,7 +780,8 @@ bool compareNonTrivial(const YAML::Node& node1, if (not node1.IsDefined() or node1.IsNull() or not node2.IsDefined() or node2.IsNull()) return false; // Find schema file - auto path_schema = findSchema(type, folders_schema); + std::stringstream log_find_schema; + auto path_schema = findSchema(type, folders_schema, log_find_schema); if (path_schema.empty()) return false; // Load schema yaml and flatten (include all nodes indicated with "follow") @@ -668,7 +809,7 @@ std::string getZeroString(const std::string& type) if (size == 0) size = 3; // arbitrary size if not specified auto lower_type = getLowerElementType(type); std::string zero_string = "["; - for (auto i = 0; i < size; i++) + for (size_t i = 0; i < size; i++) { zero_string += getZeroString(lower_type); if (i < size - 1) zero_string += ", "; @@ -710,7 +851,7 @@ std::string getZeroString(const std::string& type) } #endif } - return "type uknown!"; + return "type unknown!"; } } // namespace yaml_schema_cpp diff --git a/test/gtest_apply_schema.cpp b/test/gtest_apply_schema.cpp index eb3f834..26416ba 100644 --- a/test/gtest_apply_schema.cpp +++ b/test/gtest_apply_schema.cpp @@ -135,10 +135,186 @@ TEST(schema, complex_case) } #endif +TEST(schema, wrong_structure_map) +{ + YamlServer server({SCHEMA_DIR}); + bool succeed = true; + + // scalar where a map is expected + server.setYaml(YAML::Load("{sub_map: 5}")); + EXPECT_NO_THROW(succeed = server.applySchema("regression_map")); + EXPECT_FALSE(succeed); + EXPECT_NE(server.getLog().find("ERROR in 'sub_map'"), std::string::npos); + + // sequence where a map is expected (and it should not be converted into a map) + server.setYaml(YAML::Load("{sub_map: [1, 2]}")); + EXPECT_NO_THROW(succeed = server.applySchema("regression_map")); + EXPECT_FALSE(succeed); + EXPECT_TRUE(server.getNode()["sub_map"].IsSequence()); + + // missing and null maps are fine + for (auto input : std::vector{"{}", "{sub_map: }"}) + { + server.setYaml(YAML::Load(input)); + EXPECT_TRUE(server.applySchema("regression_map")) << input; + EXPECT_EQ(server.getNode()["sub_map"]["param"].as(), 1); + } +} + +TEST(schema, wrong_structure_derived) +{ + // derived type should be a map with 'type' + YamlServer server({SCHEMA_DIR}); + bool succeed = true; + + for (auto input : std::vector{"{final_1: a, final_derived: 5}", + "{final_1: a, final_derived: [1, 2]}", + "{final_1: a, final_derived: {type: [1, 2]}}"}) + { + server.setYaml(YAML::Load(input)); + EXPECT_NO_THROW(succeed = server.applySchema("type_derived_final")) << input; + EXPECT_FALSE(succeed) << input; + std::cout << server.getLog() << std::endl; + } +} + +TEST(schema, wrong_structure_array) +{ + // a map where an array is expected: a single error, and the map is not modified + YamlServer server({SCHEMA_DIR}); + server.setYaml(YAML::Load("{arr: {x: 1}}")); + + EXPECT_FALSE(server.applySchema("regression_array")); + std::cout << server.getLog() << std::endl; + EXPECT_NE(server.getLog().find("ERROR in 'arr'"), std::string::npos); + EXPECT_EQ(server.getLog().find("arr["), std::string::npos); + EXPECT_EQ(server.getNode()["arr"].size(), 1); +} + +TEST(schema, keys_with_slash) +{ + YamlServer server({SCHEMA_DIR}); + server.setYaml(YAML::Load("{}")); + + bool succeed = server.applySchema("regression_slash_key"); + if (not succeed) std::cout << server.getLog() << std::endl; + ASSERT_TRUE(succeed); + + YAML::Node node = server.getNode(); + std::cout << node << std::endl; + ASSERT_TRUE(node["/odom"]); + EXPECT_EQ(node["/odom"].as(), "odom_topic"); + ASSERT_TRUE(node["a/b"]); + EXPECT_EQ(node["a/b"].as(), 3); + ASSERT_TRUE(node["/map/with/slash"]); + ASSERT_TRUE(node["/map/with/slash"]["param"]); + EXPECT_EQ(node["/map/with/slash"]["param"].as(), 1); + EXPECT_FALSE(node["odom"]); + EXPECT_FALSE(node["b"]); + EXPECT_FALSE(node["slash"]); + EXPECT_EQ(node.size(), 3); +} + +TEST(schema, scalar_leaf_in_schema) +{ + // scalars in schemas are ignored + YamlServer server({SCHEMA_DIR}); + server.setYaml(YAML::Load("{}")); + + bool succeed = false; + EXPECT_NO_THROW(succeed = server.applySchema("regression_scalar_leaf")); + EXPECT_TRUE(succeed); + EXPECT_EQ(server.getNode()["param"].as(), 1); + EXPECT_FALSE(server.getNode()["some_scalar"]); +} + +TEST(schema, null_string) +{ + YamlServer server({SCHEMA_DIR}); + + // mandatory string with null value + server.setYaml(YAML::Load("name:")); + EXPECT_FALSE(server.applySchema("regression_null_string")); + std::cout << server.getLog() << std::endl; + + server.setYaml(YAML::Load("name: ~")); + EXPECT_FALSE(server.applySchema("regression_null_string")); + + // empty string is fine + server.setYaml(YAML::Load("name: ''")); + EXPECT_TRUE(server.applySchema("regression_null_string")); + + // null element of sequence of strings + server.setYaml(YAML::Load("{name: a, names: [a, ~]}")); + EXPECT_FALSE(server.applySchema("regression_null_string")); + + server.setYaml(YAML::Load("{name: a, names: [a, b]}")); + EXPECT_TRUE(server.applySchema("regression_null_string")); +} + +TEST(schema, derived_value) +{ + YamlServer server({SCHEMA_DIR}); + bool succeed; + + // same value as the schema + server.setYaml(YAML::Load("final_derived: {type: type_derived_derived, param_base: 2.4, param_derived: [1, 2]}")); + testing::internal::CaptureStdout(); + succeed = server.applySchema("regression_derived_value"); + EXPECT_EQ(testing::internal::GetCapturedStdout(), ""); + if (not succeed) std::cout << server.getLog() << std::endl; + EXPECT_TRUE(succeed); + + // different value than the schema + server.setYaml(YAML::Load("final_derived: {type: type_derived_derived, param_base: 3.0, param_derived: [1, 2]}")); + EXPECT_FALSE(server.applySchema("regression_derived_value")); + + // not defined: added from schema + server.setYaml(YAML::Load("{}")); + EXPECT_TRUE(server.applySchema("regression_derived_value")); + EXPECT_NEAR(server.getNode()["final_derived"]["param_base"].as(), 2.4, 1e-12); +} + +TEST(schema, derived_options) +{ + YamlServer server({SCHEMA_DIR}); + + server.setYaml( + YAML::Load("derived_with_options: {type: type_derived_derived, param_base: 1.0, param_derived: [1]}")); + bool succeed = server.applySchema("regression_derived_options"); + if (not succeed) std::cout << server.getLog() << std::endl; + EXPECT_TRUE(succeed); + + server.setYaml( + YAML::Load("derived_with_options: {type: type_derived_derived, param_base: 2.0, param_derived: [1]}")); + EXPECT_FALSE(server.applySchema("regression_derived_options")); + EXPECT_NE(server.getLog().find("OPTIONS"), std::string::npos); +} + +TEST(schema, string_literal_input_path) +{ + // a string literal should not be converted to bool (override) + std::string path = ROOT_DIR + "/test/yaml/base_input.yaml"; + YamlServer server({SCHEMA_DIR}, path.c_str()); + + EXPECT_TRUE(server.getNode()["map1"]); + EXPECT_TRUE(server.applySchema("base_input.schema")); +} + +TEST(schema, set_yaml_log_header) +{ + YamlServer server({SCHEMA_DIR}, ROOT_DIR + "/test/yaml/base_input.yaml"); + server.setYaml(YAML::Load("{}")); + server.applySchema("regression_map"); + + EXPECT_NE(server.getLog().find("yaml node added via setYaml"), std::string::npos); + EXPECT_EQ(server.getLog().find("base_input.yaml"), std::string::npos); +} + int main(int argc, char **argv) { testing::InitGoogleTest(&argc, argv); - ::testing::GTEST_FLAG(filter) = "schema.complex_case"; // Test only this one + //::testing::GTEST_FLAG(filter) = "schema.complex_case"; // Test only this one //::testing::GTEST_FLAG(filter) = "schema.*"; // Test only the tests in this group return RUN_ALL_TESTS(); } diff --git a/test/gtest_check_type.cpp b/test/gtest_check_type.cpp index a6331ea..307aa89 100644 --- a/test/gtest_check_type.cpp +++ b/test/gtest_check_type.cpp @@ -320,6 +320,64 @@ TEST(check_type, trivial_types) #endif } +TEST(check_type, array_types) +{ + YAML::Node node_ints = YAML::Load("[1, 2, 3]"); + + EXPECT_TRUE(tryNodeAs(node_ints, "int[]")); + EXPECT_TRUE(tryNodeAs(node_ints, "int[3]")); + EXPECT_FALSE(tryNodeAs(node_ints, "int[2]")); + EXPECT_TRUE(tryNodeAs(node_ints, "double[]")); + EXPECT_TRUE(tryNodeAs(node_ints, "string[]")); + EXPECT_FALSE(tryNodeAs(node_ints, "unknown_class[]")); + EXPECT_TRUE(checkNodeAsBasic(node_ints, "int[]")); + EXPECT_FALSE(checkNodeAsBasic(node_ints, "string[]")); + EXPECT_TRUE(checkNodeAsString(node_ints, "string[]")); + EXPECT_FALSE(checkNodeAsString(node_ints, "int[]")); + + YAML::Node node_mixed = YAML::Load("[1, a, 3]"); + EXPECT_FALSE(tryNodeAs(node_mixed, "int[]")); + EXPECT_TRUE(tryNodeAs(node_mixed, "string[]")); + + YAML::Node node_nested = YAML::Load("[[1.0, 2.0], [3.0, 4.0, 5.0]]"); + EXPECT_TRUE(tryNodeAs(node_nested, "double[2][]")); + EXPECT_FALSE(tryNodeAs(node_nested, "double[3][]")); + EXPECT_FALSE(tryNodeAs(node_nested, "double[][2]")); + EXPECT_FALSE(tryNodeAs(node_nested, "double[]")); + + EXPECT_TRUE(tryNodeAs(YAML::Load("[]"), "int[]")); + EXPECT_FALSE(tryNodeAs(YAML::Load("[]"), "int[2]")); + EXPECT_FALSE(tryNodeAs(YAML::Load("3"), "int[]")); + +#if _EIGEN_FOUND == 1 + EXPECT_TRUE(tryNodeAs(YAML::Load("[[1, 2], [3, 4]]"), "Vector2d[]")); + EXPECT_FALSE(tryNodeAs(YAML::Load("[[1, 2], [3]]"), "Vector2d[]")); +#endif +} + +TEST(check_type, malformed_types) +{ + EXPECT_FALSE(isTrivialType("int[")); + EXPECT_FALSE(isTrivialType("int]")); + EXPECT_FALSE(isTrivialType("int[x]")); + EXPECT_FALSE(isTrivialType("int][3")); + EXPECT_FALSE(isTrivialType("int[3][x]")); + EXPECT_FALSE(isBasicType("int[")); + EXPECT_FALSE(isStringType("string[")); +} + +#if _EIGEN_FOUND == 1 +TEST(check_type, eigen_no_output) +{ + // A failed conversion should not print anything + testing::internal::CaptureStdout(); + EXPECT_FALSE(tryNodeAs(YAML::Load("[1, 2]"), "Vector3d")); + EXPECT_FALSE(tryNodeAs(YAML::Load("[]"), "Vector3d")); + EXPECT_FALSE(tryNodeAs(YAML::Load("[[2, 2], [1, 2, 3]]"), "MatrixXd")); + EXPECT_EQ(testing::internal::GetCapturedStdout(), ""); +} +#endif + int main(int argc, char **argv) { testing::InitGoogleTest(&argc, argv); diff --git a/test/gtest_expression.cpp b/test/gtest_expression.cpp index c91a25b..b203b02 100644 --- a/test/gtest_expression.cpp +++ b/test/gtest_expression.cpp @@ -166,6 +166,75 @@ TEST(TestExpression, applySchemaWrong) } } +TEST(TestExpression, isExpressionEmpty) +{ + EXPECT_FALSE(isExpression("")); + EXPECT_FALSE(isExpression(YAML::Node(""))); + EXPECT_FALSE(isExpression(YAML::Node())); + EXPECT_FALSE(isExpression(YAML::Load("[1, 2]"))); + + std::string empty_str; + EXPECT_NO_THROW(preProcessExpression(empty_str)); + EXPECT_TRUE(empty_str.empty()); +} + +TEST(TestExpression, applySchemaExpressionBeforeDefault) +{ + // the expression of 'param_expr' depends on 'mode', declared after it with a default value ('auto') + YamlServer server({SCHEMA_DIR}); + + // mode not defined -> default 'auto' -> param_expr not mandatory + server.setYaml(YAML::Load("{}")); + bool succeed = server.applySchema("regression_expression_order"); + if (not succeed) std::cout << server.getLog() << std::endl; + EXPECT_TRUE(succeed); + EXPECT_EQ(server.getNode()["mode"].as(), "auto"); + + // mode 'manual' -> param_expr mandatory + server.setYaml(YAML::Load("{mode: manual}")); + EXPECT_FALSE(server.applySchema("regression_expression_order")); + + server.setYaml(YAML::Load("{mode: manual, param_expr: 3}")); + EXPECT_TRUE(server.applySchema("regression_expression_order")); +} + +TEST(TestExpression, applySchemaExpressionStringType) +{ + // 'switch_mode' is a string, although its values may look like booleans or numbers + YamlServer server({SCHEMA_DIR}); + + for (auto mode : std::vector{"on", "yes", "2", "auto"}) + { + server.setYaml(YAML::Load("{switch_mode: " + mode + "}")); + bool succeed = server.applySchema("regression_expression_string_type"); + if (not succeed) std::cout << server.getLog() << std::endl; + EXPECT_TRUE(succeed) << "switch_mode: " << mode; + } + + // 'off' -> param_off mandatory + server.setYaml(YAML::Load("{switch_mode: off}")); + EXPECT_FALSE(server.applySchema("regression_expression_string_type")); + EXPECT_NE(server.getLog().find("Missing mandatory field"), std::string::npos); + + server.setYaml(YAML::Load("{switch_mode: off, param_off: 1}")); + EXPECT_TRUE(server.applySchema("regression_expression_string_type")); +} + +TEST(TestExpression, applySchemaExpressionEvalFails) +{ + // 'mode' is missing, so the expression of 'param' cannot be evaluated + YamlServer server({SCHEMA_DIR}); + server.setYaml(YAML::Load("{}")); + + EXPECT_FALSE(server.applySchema("regression_expression_eval_fail")); + std::string log = server.getLog(); + std::cout << log << std::endl; + EXPECT_NE(log.find("ERROR in 'mode': Missing mandatory field"), std::string::npos); + EXPECT_NE(log.find("ERROR in 'param': Evaluating schema expression"), std::string::npos); + EXPECT_EQ(log.find("ERROR in 'param': Missing mandatory field"), std::string::npos); + EXPECT_FALSE(server.getNode()["param"]); +} + int main(int argc, char** argv) { testing::InitGoogleTest(&argc, argv); diff --git a/test/gtest_flatten.cpp b/test/gtest_flatten.cpp index 18d2f08..b1d214c 100644 --- a/test/gtest_flatten.cpp +++ b/test/gtest_flatten.cpp @@ -147,7 +147,34 @@ TEST(flatten_schema, sequence_follow) ASSERT_TRUE(compareNodesAutoType(schema_flatten, gt_node)); // compareNodesAutoType validated at gtest_yaml_utils } -int main(int argc, char **argv) +TEST(flatten_yaml, follow_cycle) +{ + // a.yaml follows b.yaml, that follows a.yaml + try + { + YamlServer yaml_server({SCHEMA_DIR}, ROOT_DIR + "/test/yaml/follow_cycle/a.yaml"); + FAIL() << "A 'follow' cycle should throw"; + } + catch (const std::runtime_error& e) + { + std::cout << e.what() << std::endl; + EXPECT_NE(std::string(e.what()).find("cycle"), std::string::npos) << e.what(); + } +} + +TEST(flatten_yaml, follow_diamond) +{ + // left.yaml and right.yaml follow common.yaml: not a cycle + YAML::Node node; + ASSERT_NO_THROW(node = YamlServer({SCHEMA_DIR}, ROOT_DIR + "/test/yaml/follow_diamond/main.yaml").getNode()); + + EXPECT_EQ(node["main_param"].as(), 0); + EXPECT_EQ(node["left_param"].as(), 1); + EXPECT_EQ(node["right_param"].as(), 2); + EXPECT_EQ(node["common_param"].as(), 3); +} + +int main(int argc, char** argv) { testing::InitGoogleTest(&argc, argv); //::testing::GTEST_FLAG(filter) = "TestTest.DummyTestExample"; // Test only this one diff --git a/test/gtest_generator.cpp b/test/gtest_generator.cpp index 0cf7abb..9e5dd70 100644 --- a/test/gtest_generator.cpp +++ b/test/gtest_generator.cpp @@ -3,6 +3,9 @@ #include "yaml-schema-cpp/filesystem_wrapper.hpp" #include "yaml-schema-cpp/yaml_generator.hpp" +#include +#include + std::string ROOT_DIR = _YAML_SCHEMA_CPP_ROOT_DIR; using namespace yaml_schema_cpp; @@ -79,6 +82,65 @@ TEST(generator, file_template) filesystem::remove_all(temporary_folder); } +TEST(generator, no_output) +{ + testing::internal::CaptureStdout(); + generateYaml("test1.schema", {ROOT_DIR + "/test/schema"}); + generateYaml("type_derived_final.schema", {ROOT_DIR + "/test/schema"}); + EXPECT_EQ(testing::internal::GetCapturedStdout(), ""); +} + +TEST(generator, scalar_leaf_in_schema) +{ + YAML::Node node; + ASSERT_NO_THROW(node = generateYaml("regression_scalar_leaf", {ROOT_DIR + "/test/schema"})); + EXPECT_TRUE(node["param"]); + EXPECT_FALSE(node["some_scalar"]); +} + +TEST(generator, template_override) +{ + std::string temporary_folder = ROOT_DIR + "/test/temp_folder_override"; + if (not filesystem::exists(temporary_folder)) filesystem::create_directory(temporary_folder); + + // 'same_param' has the same parameter twice: only valid if override + EXPECT_NO_THROW( + generateTemplate(temporary_folder + "/same_param.yaml", "same_param", {ROOT_DIR + "/test/schema"})); + EXPECT_THROW( + generateTemplate(temporary_folder + "/same_param.yaml", "same_param", {ROOT_DIR + "/test/schema"}, false), + std::runtime_error); + + filesystem::remove_all(temporary_folder); +} + +TEST(generator, template_quotes) +{ + std::string temporary_folder = ROOT_DIR + "/test/temp_folder_quotes"; + if (not filesystem::exists(temporary_folder)) filesystem::create_directory(temporary_folder); + + auto file_path = generateTemplate( + temporary_folder + "/template_quotes.yaml", "regression_template_quotes", {ROOT_DIR + "/test/schema"}); + + // file content + std::ifstream file(file_path); + std::stringstream content; + content << file.rdbuf(); + std::cout << content.str() << std::endl; + + EXPECT_NE(content.str().find("the owner's name, e.g. \"John\""), std::string::npos); + EXPECT_NE(content.str().find("a doc on two lines"), std::string::npos); + + // the generated template is a valid YAML with the expected values + YAML::Node node; + ASSERT_NO_THROW(node = YAML::LoadFile(file_path)); + EXPECT_EQ(node["name"].as(), "whatever"); + EXPECT_EQ(node["mode"].as(), "auto"); + EXPECT_EQ(node["count"].as(), 3); + EXPECT_EQ(node.size(), 3); + + filesystem::remove_all(temporary_folder); +} + int main(int argc, char **argv) { testing::InitGoogleTest(&argc, argv); diff --git a/test/gtest_relative_path.cpp b/test/gtest_relative_path.cpp index 47fe5fe..9ae4970 100644 --- a/test/gtest_relative_path.cpp +++ b/test/gtest_relative_path.cpp @@ -40,7 +40,70 @@ TEST(relative_path, other_folder) ASSERT_TRUE(filesystem::exists(input_node["map1"]["param2"].as())); } -int main(int argc, char **argv) +// Changes the current working directory during the life of the object +struct ScopedCurrentPath +{ + filesystem::path previous_; + ScopedCurrentPath(const std::string& path) : previous_(filesystem::current_path()) + { + filesystem::current_path(path); + } + ~ScopedCurrentPath() + { + filesystem::current_path(previous_); + } +}; + +void checkRelativeFollow(const YAML::Node& input_node) +{ + std::cout << "input_node: " << std::endl << input_node << std::endl; + + ASSERT_TRUE(input_node["local_file"]); + ASSERT_TRUE(input_node["sub_file"]); + ASSERT_TRUE(input_node["sub_map"]); + ASSERT_TRUE(input_node["sub_map"]["main_nested_file"]); + ASSERT_TRUE(input_node["sub_map"]["sub_nested_file"]); + + EXPECT_TRUE(filesystem::exists(input_node["local_file"].as())); + EXPECT_TRUE(filesystem::exists(input_node["sub_file"].as())); + EXPECT_TRUE(filesystem::exists(input_node["sub_map"]["main_nested_file"].as())); + EXPECT_TRUE(filesystem::exists(input_node["sub_map"]["sub_nested_file"].as())); +} + +TEST(relative_path, relative_input_path) +{ + ScopedCurrentPath scoped_path(ROOT_DIR + "/test/yaml"); + + yaml_schema_cpp::YamlServer yaml_server({}, "./relative_follow/main.yaml"); + YAML::Node input_node = yaml_server.getNode(); + + checkRelativeFollow(input_node); + EXPECT_EQ(input_node["sub_file"].as(), "./relative_follow/sub/./sub.yaml"); +} + +TEST(relative_path, bare_input_file_name) +{ + ScopedCurrentPath scoped_path(ROOT_DIR + "/test/yaml/relative_follow"); + + YAML::Node input_node; + ASSERT_NO_THROW(input_node = yaml_schema_cpp::YamlServer({}, "main.yaml").getNode()); + + checkRelativeFollow(input_node); +} + +TEST(relative_path, absolute_follow) +{ + YAML::Node node = YAML::Load("follow: " + ROOT_DIR + "/test/yaml/relative_follow/sub/sub.yaml"); + + ASSERT_NO_THROW(flattenNode(node, ROOT_DIR + "/test/yaml", {}, false, true)); + + std::cout << "node: " << std::endl << node << std::endl; + ASSERT_TRUE(node["sub_file"]); + EXPECT_EQ(node["sub_file"].as(), ROOT_DIR + "/test/yaml/relative_follow/sub/./sub.yaml"); + EXPECT_TRUE(filesystem::exists(node["sub_file"].as())); +} + +int main(int argc, char** argv) { testing::InitGoogleTest(&argc, argv); //::testing::GTEST_FLAG(filter) = "TestTest.DummyTestExample"; // Test only this one diff --git a/test/gtest_schema.cpp b/test/gtest_schema.cpp index c1fd3c2..fe9f8a5 100644 --- a/test/gtest_schema.cpp +++ b/test/gtest_schema.cpp @@ -61,7 +61,9 @@ TEST(schema, wrong) "wrong_options", "wrong_options2", "wrong_value_options", - "wrong_value"}; + "wrong_value", + "value_without_doc", + "follow_cycle"}; for (auto w_schema : wrong_schemas) { diff --git a/test/gtest_yaml_utils.cpp b/test/gtest_yaml_utils.cpp index 9e695a6..5b4f1ae 100644 --- a/test/gtest_yaml_utils.cpp +++ b/test/gtest_yaml_utils.cpp @@ -242,6 +242,79 @@ TEST(compare, compare_nodes_auto_type) EXPECT_FALSE(compareNodesAutoType(node_input, node_comp)); } +TEST(compare, compare_nodes_auto_type_null) +{ + EXPECT_TRUE(compareNodesAutoType(YAML::Node(), YAML::Node())); + EXPECT_TRUE(compareNodesAutoType(YAML::Node(YAML::NodeType::Undefined), YAML::Node(YAML::NodeType::Undefined))); + EXPECT_TRUE(compareNodesAutoType(YAML::Load("{a: ~, b: [1, ~]}"), YAML::Load("{a: ~, b: [1, ~]}"))); + EXPECT_FALSE(compareNodesAutoType(YAML::Load("{a: ~}"), YAML::Load("{a: 1}"))); + EXPECT_FALSE(compareNodesAutoType(YAML::Node(), YAML::Node(YAML::NodeType::Undefined))); +} + +TEST(compare, compare_non_trivial_expression_mandatory) +{ + // expression.schema contains parameters with expressions in _mandatory + YAML::Node node_input = YAML::LoadFile(ROOT_DIR + "/test/yaml/expression_input1.yaml"); + YAML::Node node_comp = YAML::Clone(node_input); + + EXPECT_TRUE(compare(node_input, node_comp, "expression", {SCHEMA_DIR})); + + node_comp["param_int"] = 1000; + EXPECT_FALSE(compare(node_input, node_comp, "expression", {SCHEMA_DIR})); + + // scalar where a map is expected: not equal (and no exception) + EXPECT_FALSE(compare(node_input, YAML::Node("scalar"), "expression", {SCHEMA_DIR})); +} + +TEST(compare, compare_non_trivial_no_output) +{ + // a schema not found should not print anything + testing::internal::CaptureStdout(); + EXPECT_FALSE(compare(YAML::Node("a"), YAML::Node("a"), "non_existing_type", {SCHEMA_DIR})); + EXPECT_EQ(testing::internal::GetCapturedStdout(), ""); +} + +#if _EIGEN_FOUND == 1 +TEST(compare, compare_eigen_dynamic) +{ + YAML::Node vec3 = YAML::Load("[1, 2, 3]"); + YAML::Node vec2 = YAML::Load("[1, 2]"); + YAML::Node mat = YAML::Load("[[2, 2], [1, 2, 3, 4]]"); + YAML::Node mat2 = YAML::Load("[[2, 2], [1, 2, 3, 5]]"); + YAML::Node mat3 = YAML::Load("[[1, 4], [1, 2, 3, 4]]"); + + for (auto type : std::vector{"VectorXd", "Eigen::VectorXd"}) + { + EXPECT_TRUE(compare(vec3, vec3, type, {})); + EXPECT_FALSE(compare(vec3, vec2, type, {})); // different sizes + EXPECT_FALSE(compare(vec2, vec3, type, {})); // different sizes + EXPECT_TRUE(compare(YAML::Load("[]"), YAML::Load("[]"), type, {})); + EXPECT_FALSE(compare(YAML::Load("[]"), vec2, type, {})); + } + for (auto type : std::vector{"MatrixXd", "Eigen::MatrixXd"}) + { + EXPECT_TRUE(compare(mat, mat, type, {})); + EXPECT_FALSE(compare(mat, mat2, type, {})); // different values + EXPECT_FALSE(compare(mat, mat3, type, {})); // different sizes + } +} +#endif + +TEST(TestYamlUtils, getAllSchemas_missing_folder) +{ + std::vector schemas; + EXPECT_NO_THROW(schemas = getAllSchemas({ROOT_DIR + "/test/non_existing_folder", SCHEMA_DIR})); + EXPECT_FALSE(schemas.empty()); +} + +TEST(TestYamlUtils, isArrayType_malformed) +{ + EXPECT_THROW(isArrayType("int[x]"), std::runtime_error); + EXPECT_THROW(isArrayType("int[-1]"), std::runtime_error); + EXPECT_THROW(isArrayType("int][3"), std::runtime_error); + EXPECT_THROW(isArrayType("int[3a]"), std::runtime_error); +} + int main(int argc, char **argv) { testing::InitGoogleTest(&argc, argv); diff --git a/test/schema/regression/regression_array.schema b/test/schema/regression/regression_array.schema new file mode 100644 index 0000000..abfdc38 --- /dev/null +++ b/test/schema/regression/regression_array.schema @@ -0,0 +1,4 @@ +arr: + _mandatory: true + _type: int[] + _doc: array of ints diff --git a/test/schema/regression/regression_derived_options.schema b/test/schema/regression/regression_derived_options.schema new file mode 100644 index 0000000..50ef596 --- /dev/null +++ b/test/schema/regression/regression_derived_options.schema @@ -0,0 +1,9 @@ +derived_with_options: + _mandatory: true + _type: derived + _base: type_derived_base.schema + _options: + - type: type_derived_derived + param_base: 1.0 + param_derived: [1] + _doc: derived type with options diff --git a/test/schema/regression/regression_derived_value.schema b/test/schema/regression/regression_derived_value.schema new file mode 100644 index 0000000..c2518da --- /dev/null +++ b/test/schema/regression/regression_derived_value.schema @@ -0,0 +1,9 @@ +final_derived: + _mandatory: false + _type: derived + _base: type_derived_base.schema + _value: + type: type_derived_derived + param_base: 2.4 + param_derived: [1, 2] + _doc: derived type with value diff --git a/test/schema/regression/regression_expression_eval_fail.schema b/test/schema/regression/regression_expression_eval_fail.schema new file mode 100644 index 0000000..6ba126c --- /dev/null +++ b/test/schema/regression/regression_expression_eval_fail.schema @@ -0,0 +1,8 @@ +mode: + _mandatory: true + _type: string + _doc: mandatory string +param: + _mandatory: $mode == 'auto' + _type: int + _doc: mandatory if mode is 'auto' diff --git a/test/schema/regression/regression_expression_order.schema b/test/schema/regression/regression_expression_order.schema new file mode 100644 index 0000000..61128e5 --- /dev/null +++ b/test/schema/regression/regression_expression_order.schema @@ -0,0 +1,10 @@ +# The expression of 'param_expr' depends on 'mode', which is declared AFTER it and has a default value +param_expr: + _mandatory: $mode == 'manual' + _type: int + _doc: mandatory only if mode is 'manual' +mode: + _mandatory: false + _type: string + _default: auto + _doc: mode with default value diff --git a/test/schema/regression/regression_expression_string_type.schema b/test/schema/regression/regression_expression_string_type.schema new file mode 100644 index 0000000..331109a --- /dev/null +++ b/test/schema/regression/regression_expression_string_type.schema @@ -0,0 +1,9 @@ +# 'switch_mode' is a string whose values may look like booleans or numbers ('on', 'off', '2') +switch_mode: + _mandatory: true + _type: string + _doc: string parameter +param_off: + _mandatory: $switch_mode == 'off' + _type: int + _doc: mandatory only if switch_mode is 'off' diff --git a/test/schema/regression/regression_map.schema b/test/schema/regression/regression_map.schema new file mode 100644 index 0000000..a8f93d1 --- /dev/null +++ b/test/schema/regression/regression_map.schema @@ -0,0 +1,6 @@ +sub_map: + param: + _mandatory: false + _type: int + _default: 1 + _doc: some param diff --git a/test/schema/regression/regression_null_string.schema b/test/schema/regression/regression_null_string.schema new file mode 100644 index 0000000..785efc8 --- /dev/null +++ b/test/schema/regression/regression_null_string.schema @@ -0,0 +1,8 @@ +name: + _mandatory: true + _type: string + _doc: mandatory string +names: + _mandatory: false + _type: string[] + _doc: optional sequence of strings diff --git a/test/schema/regression/regression_scalar_leaf.schema b/test/schema/regression/regression_scalar_leaf.schema new file mode 100644 index 0000000..d9bfc90 --- /dev/null +++ b/test/schema/regression/regression_scalar_leaf.schema @@ -0,0 +1,7 @@ +# scalar nodes in schemas are ignored +some_scalar: whatever +param: + _mandatory: false + _type: int + _default: 1 + _doc: some param diff --git a/test/schema/regression/regression_slash_key.schema b/test/schema/regression/regression_slash_key.schema new file mode 100644 index 0000000..1b5cfea --- /dev/null +++ b/test/schema/regression/regression_slash_key.schema @@ -0,0 +1,17 @@ +# keys containing '/' (e.g. ROS topic names) +"/odom": + _mandatory: false + _type: string + _default: odom_topic + _doc: key containing a slash, with default +"a/b": + _mandatory: false + _type: int + _value: 3 + _doc: key containing a slash, with value +"/map/with/slash": + param: + _mandatory: false + _type: int + _default: 1 + _doc: param inside a map whose key contains slashes diff --git a/test/schema/regression/regression_template_quotes.schema b/test/schema/regression/regression_template_quotes.schema new file mode 100644 index 0000000..201f21e --- /dev/null +++ b/test/schema/regression/regression_template_quotes.schema @@ -0,0 +1,17 @@ +name: + _mandatory: true + _type: string + _doc: "the owner's name, e.g. \"John\"" +mode: + _mandatory: false + _type: string + _default: auto + _options: [auto, manual] + _doc: 'mode: auto or manual' +count: + _mandatory: false + _type: int + _default: 3 + _doc: | + a doc + on two lines diff --git a/test/wrong_schema/follow_cycle.schema b/test/wrong_schema/follow_cycle.schema new file mode 100644 index 0000000..63fd6d3 --- /dev/null +++ b/test/wrong_schema/follow_cycle.schema @@ -0,0 +1,6 @@ +# follow_cycle.schema and follow_cycle2.schema follow each other +follow: follow_cycle2 +param1: + _mandatory: true + _type: int + _doc: some doc diff --git a/test/wrong_schema/follow_cycle2.schema b/test/wrong_schema/follow_cycle2.schema new file mode 100644 index 0000000..8b1b7c1 --- /dev/null +++ b/test/wrong_schema/follow_cycle2.schema @@ -0,0 +1,6 @@ +# follow_cycle.schema and follow_cycle2.schema follow each other +follow: follow_cycle +param2: + _mandatory: true + _type: int + _doc: some doc diff --git a/test/wrong_schema/value_without_doc.schema b/test/wrong_schema/value_without_doc.schema new file mode 100644 index 0000000..ba80085 --- /dev/null +++ b/test/wrong_schema/value_without_doc.schema @@ -0,0 +1,7 @@ +# '_value' is a reserved key: it is not allowed in a node that is not a specification +param: + _value: 3 + sub: + _mandatory: false + _type: int + _doc: some doc diff --git a/test/yaml/follow_cycle/a.yaml b/test/yaml/follow_cycle/a.yaml new file mode 100644 index 0000000..44a5d53 --- /dev/null +++ b/test/yaml/follow_cycle/a.yaml @@ -0,0 +1,2 @@ +param_a: 1 +follow: b.yaml diff --git a/test/yaml/follow_cycle/b.yaml b/test/yaml/follow_cycle/b.yaml new file mode 100644 index 0000000..00d8833 --- /dev/null +++ b/test/yaml/follow_cycle/b.yaml @@ -0,0 +1,2 @@ +param_b: 2 +follow: a.yaml diff --git a/test/yaml/follow_diamond/common.yaml b/test/yaml/follow_diamond/common.yaml new file mode 100644 index 0000000..2103d34 --- /dev/null +++ b/test/yaml/follow_diamond/common.yaml @@ -0,0 +1 @@ +common_param: 3 diff --git a/test/yaml/follow_diamond/left.yaml b/test/yaml/follow_diamond/left.yaml new file mode 100644 index 0000000..cb2ff73 --- /dev/null +++ b/test/yaml/follow_diamond/left.yaml @@ -0,0 +1,2 @@ +follow: common.yaml +left_param: 1 diff --git a/test/yaml/follow_diamond/main.yaml b/test/yaml/follow_diamond/main.yaml new file mode 100644 index 0000000..f2dda53 --- /dev/null +++ b/test/yaml/follow_diamond/main.yaml @@ -0,0 +1,3 @@ +# both left.yaml and right.yaml follow common.yaml (not a cycle) +follow: [left.yaml, right.yaml] +main_param: 0 diff --git a/test/yaml/follow_diamond/right.yaml b/test/yaml/follow_diamond/right.yaml new file mode 100644 index 0000000..6b5baa8 --- /dev/null +++ b/test/yaml/follow_diamond/right.yaml @@ -0,0 +1,2 @@ +follow: common.yaml +right_param: 2 diff --git a/test/yaml/relative_follow/main.yaml b/test/yaml/relative_follow/main.yaml new file mode 100644 index 0000000..74fbc67 --- /dev/null +++ b/test/yaml/relative_follow/main.yaml @@ -0,0 +1,4 @@ +follow: sub/sub.yaml +local_file: ./main.yaml +sub_map: + main_nested_file: ./main.yaml diff --git a/test/yaml/relative_follow/sub/sub.yaml b/test/yaml/relative_follow/sub/sub.yaml new file mode 100644 index 0000000..a030f7e --- /dev/null +++ b/test/yaml/relative_follow/sub/sub.yaml @@ -0,0 +1,3 @@ +sub_file: ./sub.yaml +sub_map: + sub_nested_file: ../main.yaml