diff --git a/.github/workflows/website-build.yml b/.github/workflows/website-build.yml index 99e7fdd30..387e09aae 100644 --- a/.github/workflows/website-build.yml +++ b/.github/workflows/website-build.yml @@ -22,6 +22,7 @@ jobs: -DBLAZE_TEST:BOOL=OFF -DBLAZE_CONFIGURATION:BOOL=OFF -DBLAZE_ALTERSCHEMA:BOOL=OFF + -DBLAZE_DOCUMENTATION:BOOL=OFF -DBLAZE_TESTS:BOOL=OFF -DBLAZE_DOCS:BOOL=ON - run: cmake --build ./build --config Release --target doxygen diff --git a/.github/workflows/website-deploy.yml b/.github/workflows/website-deploy.yml index 180e16d02..0e9338dfd 100644 --- a/.github/workflows/website-deploy.yml +++ b/.github/workflows/website-deploy.yml @@ -33,6 +33,7 @@ jobs: -DBLAZE_TEST:BOOL=OFF -DBLAZE_CONFIGURATION:BOOL=OFF -DBLAZE_ALTERSCHEMA:BOOL=OFF + -DBLAZE_DOCUMENTATION:BOOL=OFF -DBLAZE_TESTS:BOOL=OFF -DBLAZE_DOCS:BOOL=ON - run: cmake --build ./build --config Release --target doxygen diff --git a/CMakeLists.txt b/CMakeLists.txt index 8a4459fdf..adf16467e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,7 @@ option(BLAZE_OUTPUT "Build the Blaze output formats library" ON) option(BLAZE_TEST "Build the Blaze test runner library" ON) option(BLAZE_CONFIGURATION "Build the Blaze configuration file library" ON) option(BLAZE_ALTERSCHEMA "Build the Blaze alterschema rule library" ON) +option(BLAZE_DOCUMENTATION "Build the Blaze documentation generator library" ON) option(BLAZE_TESTS "Build the Blaze tests" OFF) option(BLAZE_BENCHMARK "Build the Blaze benchmarks" OFF) option(BLAZE_CONTRIB "Build the Blaze contrib programs" OFF) @@ -67,6 +68,10 @@ if(BLAZE_ALTERSCHEMA) add_subdirectory(src/alterschema) endif() +if(BLAZE_DOCUMENTATION) + add_subdirectory(src/documentation) +endif() + if(BLAZE_CONTRIB) add_subdirectory(contrib) endif() @@ -134,6 +139,10 @@ if(BLAZE_TESTS) add_subdirectory(test/alterschema) endif() + if(BLAZE_DOCUMENTATION) + add_subdirectory(test/documentation) + endif() + if(PROJECT_IS_TOP_LEVEL) # Otherwise we need the child project to link # against the sanitizers too. diff --git a/config.cmake.in b/config.cmake.in index 8bcccb78b..01c8d7ee0 100644 --- a/config.cmake.in +++ b/config.cmake.in @@ -10,6 +10,7 @@ if(NOT BLAZE_COMPONENTS) list(APPEND BLAZE_COMPONENTS test) list(APPEND BLAZE_COMPONENTS configuration) list(APPEND BLAZE_COMPONENTS alterschema) + list(APPEND BLAZE_COMPONENTS documentation) endif() include(CMakeFindDependencyMacro) @@ -35,6 +36,8 @@ foreach(component ${BLAZE_COMPONENTS}) include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_compiler.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_output.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_alterschema.cmake") + elseif(component STREQUAL "documentation") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_documentation.cmake") else() message(FATAL_ERROR "Unknown Blaze component: ${component}") endif() diff --git a/doxygen/index.markdown b/doxygen/index.markdown index 2ec9d4ad8..119268eba 100644 --- a/doxygen/index.markdown +++ b/doxygen/index.markdown @@ -71,6 +71,7 @@ CMake | `BLAZE_EVALUATOR` | Boolean | `ON` | Build the Blaze evaluator library | | `BLAZE_TEST` | Boolean | `ON` | Build the Blaze test runner library | | `BLAZE_ALTERSCHEMA` | Boolean | `ON` | Build the Blaze alterschema rule library| +| `BLAZE_DOCUMENTATION` | Boolean | `ON` | Build the Blaze documentation library | | `BLAZE_TESTS` | Boolean | `OFF` | Build the Blaze tests | | `BLAZE_BENCHMARK` | Boolean | `OFF` | Build the Blaze benchmarks | | `BLAZE_CONTRIB` | Boolean | `OFF` | Build the Blaze contrib programs | diff --git a/src/documentation/CMakeLists.txt b/src/documentation/CMakeLists.txt new file mode 100644 index 000000000..4e33b1ff6 --- /dev/null +++ b/src/documentation/CMakeLists.txt @@ -0,0 +1,15 @@ +sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME documentation + FOLDER "Blaze/Documentation" + PRIVATE_HEADERS error.h + SOURCES documentation.cc) + +if(BLAZE_INSTALL) + sourcemeta_library_install(NAMESPACE sourcemeta PROJECT blaze NAME documentation) +endif() + +target_link_libraries(sourcemeta_blaze_documentation PUBLIC + sourcemeta::core::json) +target_link_libraries(sourcemeta_blaze_documentation PUBLIC + sourcemeta::core::jsonschema) +target_link_libraries(sourcemeta_blaze_documentation PRIVATE + sourcemeta::blaze::alterschema) diff --git a/src/documentation/documentation.cc b/src/documentation/documentation.cc new file mode 100644 index 000000000..8d9216867 --- /dev/null +++ b/src/documentation/documentation.cc @@ -0,0 +1,1006 @@ +#include + +#include + +#include + +#include // std::array +#include // assert +#include // std::map +#include // std::ostringstream +#include // std::to_string +#include // std::move + +namespace sourcemeta::blaze { + +namespace { + +auto resolve_destination(const sourcemeta::core::JSON::String &raw_ref, + const sourcemeta::core::SchemaFrame &frame) + -> std::optional< + std::reference_wrapper> { + auto result{frame.traverse(raw_ref)}; + if (result.has_value()) { + return result; + } + for (const auto &[key, entry] : frame.references()) { + if (key.first == sourcemeta::core::SchemaReferenceType::Static && + entry.original == raw_ref) { + return frame.traverse(entry.destination); + } + } + return std::nullopt; +} + +using VisitedSchemas = std::map; + +auto type_expression_of(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, + const VisitedSchemas &visited) + -> Documentation::Type::Expression { + Documentation::Type::Expression expression; + + if (schema.is_boolean()) { + if (schema.to_boolean()) { + expression.value = Documentation::Type::Expression::Any{}; + } else { + expression.value = Documentation::Type::Expression::Never{}; + } + return expression; + } + + if (!schema.is_object()) { + return expression; + } + + if (schema.defines("$ref") && schema.at("$ref").is_string()) { + const auto &destination{schema.at("$ref").to_string()}; + const auto target{resolve_destination(destination, frame)}; + if (!target.has_value()) { + Documentation::Type::Expression::ExternalReference reference; + reference.url = destination; + expression.value = std::move(reference); + return expression; + } + const auto &target_schema{ + sourcemeta::core::get(root, target->get().pointer)}; + const auto visited_entry{visited.find(&target_schema)}; + if (visited_entry != visited.end()) { + Documentation::Type::Expression::RecursiveReference reference; + reference.identifier = visited_entry->second; + expression.value = reference; + return expression; + } + } + + if (schema.defines("$dynamicRef") && schema.at("$dynamicRef").is_string()) { + const auto &value{schema.at("$dynamicRef").to_string()}; + Documentation::Type::Expression::DynamicReference reference; + const auto fragment_start{value.find('#')}; + if (fragment_start != sourcemeta::core::JSON::String::npos) { + reference.anchor = value.substr(fragment_start + 1); + } else { + reference.anchor = value; + } + expression.value = std::move(reference); + return expression; + } + + if (schema.defines("enum") && schema.at("enum").is_array()) { + Documentation::Type::Expression::Enumeration enumeration; + std::size_t index{0}; + for (const auto &value : schema.at("enum").as_array()) { + if (index < 10) { + enumeration.values.push_back(value); + } else { + enumeration.overflow.push_back(value); + } + ++index; + } + expression.value = std::move(enumeration); + return expression; + } + + if (schema.defines("type") && schema.at("type").is_string()) { + const auto &type{schema.at("type").to_string()}; + if (type == "object") { + expression.value = Documentation::Type::Expression::Object{}; + } else if (type == "array") { + if (schema.defines("prefixItems") && + schema.at("prefixItems").is_array()) { + Documentation::Type::Expression::Tuple tuple; + for (const auto &item : schema.at("prefixItems").as_array()) { + tuple.items.push_back(type_expression_of(item, frame, root, visited)); + } + if (schema.defines("items") && schema.at("items").is_object()) { + tuple.additional.push_back( + type_expression_of(schema.at("items"), frame, root, visited)); + } else if (schema.defines("unevaluatedItems") && + schema.at("unevaluatedItems").is_object()) { + tuple.additional.push_back(type_expression_of( + schema.at("unevaluatedItems"), frame, root, visited)); + } + expression.value = std::move(tuple); + } else { + Documentation::Type::Expression::Array array; + if (schema.defines("items") && schema.at("items").is_object()) { + array.push_back( + type_expression_of(schema.at("items"), frame, root, visited)); + } + expression.value = std::move(array); + } + } else if (type == "string") { + expression.value = Documentation::Type::Expression::Primitive::String; + } else if (type == "integer") { + expression.value = Documentation::Type::Expression::Primitive::Integer; + } else if (type == "number") { + expression.value = Documentation::Type::Expression::Primitive::Number; + } + } + + return expression; +} + +auto badges_of(const sourcemeta::core::JSON &schema) + -> std::vector { + std::vector badges; + if (!schema.is_object()) { + return badges; + } + if (schema.defines("format") && schema.at("format").is_string()) { + badges.push_back( + {Documentation::Badge::Kind::Format, schema.at("format").to_string()}); + } + if (schema.defines("contentEncoding") && + schema.at("contentEncoding").is_string()) { + badges.push_back({Documentation::Badge::Kind::Encoding, + schema.at("contentEncoding").to_string()}); + } + if (schema.defines("contentMediaType") && + schema.at("contentMediaType").is_string()) { + badges.push_back({Documentation::Badge::Kind::Mime, + schema.at("contentMediaType").to_string()}); + } + return badges; +} + +auto notes_of(const sourcemeta::core::JSON &schema) -> Documentation::Notes { + Documentation::Notes notes; + if (!schema.is_object()) { + return notes; + } + if (schema.defines("title") && schema.at("title").is_string()) { + notes.title = schema.at("title").to_string(); + } + if (schema.defines("description") && schema.at("description").is_string()) { + notes.description = schema.at("description").to_string(); + } + if (schema.defines("default")) { + notes.default_value = schema.at("default"); + } + if (schema.defines("examples") && schema.at("examples").is_array()) { + for (const auto &example : schema.at("examples").as_array()) { + notes.examples.push_back(example); + } + } + return notes; +} + +auto modifiers_of(const sourcemeta::core::JSON &schema) + -> std::vector { + std::vector modifiers; + if (!schema.is_object()) { + return modifiers; + } + if (schema.defines("readOnly") && schema.at("readOnly").is_boolean() && + schema.at("readOnly").to_boolean()) { + modifiers.push_back(Documentation::Modifier::ReadOnly); + } + if (schema.defines("writeOnly") && schema.at("writeOnly").is_boolean() && + schema.at("writeOnly").to_boolean()) { + modifiers.push_back(Documentation::Modifier::WriteOnly); + } + if (schema.defines("deprecated") && schema.at("deprecated").is_boolean() && + schema.at("deprecated").to_boolean()) { + modifiers.push_back(Documentation::Modifier::Deprecated); + } + return modifiers; +} + +auto format_json_number(const sourcemeta::core::JSON &value) + -> sourcemeta::core::JSON::String { + std::ostringstream result; + sourcemeta::core::stringify(value, result); + return result.str(); +} + +auto constraints_of(const sourcemeta::core::JSON &schema) + -> std::vector { + std::vector constraints; + if (!schema.is_object()) { + return constraints; + } + + const auto has_min_length{schema.defines("minLength") && + schema.at("minLength").is_integer()}; + const auto has_max_length{schema.defines("maxLength") && + schema.at("maxLength").is_integer()}; + if (has_min_length && has_max_length && + schema.at("minLength") == schema.at("maxLength")) { + const auto value{schema.at("minLength").to_integer()}; + if (value != 0) { + constraints.emplace_back("exactly " + std::to_string(value) + " chars"); + } + } else { + if (has_min_length) { + const auto value{schema.at("minLength").to_integer()}; + if (value > 0) { + constraints.emplace_back(">= " + std::to_string(value) + " chars"); + } + } + if (has_max_length) { + constraints.emplace_back( + "<= " + std::to_string(schema.at("maxLength").to_integer()) + + " chars"); + } + } + + if (schema.defines("minimum") && schema.at("minimum").is_number()) { + constraints.emplace_back(">= " + format_json_number(schema.at("minimum"))); + } + if (schema.defines("maximum") && schema.at("maximum").is_number()) { + constraints.emplace_back("<= " + format_json_number(schema.at("maximum"))); + } + if (schema.defines("exclusiveMinimum") && + schema.at("exclusiveMinimum").is_number()) { + constraints.emplace_back("> " + + format_json_number(schema.at("exclusiveMinimum"))); + } + if (schema.defines("exclusiveMaximum") && + schema.at("exclusiveMaximum").is_number()) { + constraints.emplace_back("< " + + format_json_number(schema.at("exclusiveMaximum"))); + } + + if (schema.defines("multipleOf") && schema.at("multipleOf").is_number()) { + const auto &value{schema.at("multipleOf")}; + if (!value.is_integer() || value.to_integer() != 1) { + constraints.emplace_back("multiple of " + format_json_number(value)); + } + } + + if (schema.defines("minItems") && schema.at("minItems").is_integer()) { + const auto value{schema.at("minItems").to_integer()}; + if (value > 0) { + constraints.emplace_back(">= " + std::to_string(value) + " items"); + } + } + if (schema.defines("maxItems") && schema.at("maxItems").is_integer()) { + constraints.emplace_back( + "<= " + std::to_string(schema.at("maxItems").to_integer()) + " items"); + } + + if (schema.defines("uniqueItems") && schema.at("uniqueItems").is_boolean() && + schema.at("uniqueItems").to_boolean()) { + constraints.emplace_back("unique"); + } + + if (schema.defines("minProperties") && + schema.at("minProperties").is_integer()) { + const auto value{schema.at("minProperties").to_integer()}; + if (value > 0) { + constraints.emplace_back(">= " + std::to_string(value) + " properties"); + } + } + if (schema.defines("maxProperties") && + schema.at("maxProperties").is_integer()) { + constraints.emplace_back( + "<= " + std::to_string(schema.at("maxProperties").to_integer()) + + " properties"); + } + + if (schema.defines("pattern") && schema.at("pattern").is_string()) { + constraints.emplace_back("pattern: " + schema.at("pattern").to_string()); + } + + const auto has_trivial_contains{schema.defines("contains") && + schema.at("contains").is_boolean() && + schema.at("contains").to_boolean()}; + + if (!has_trivial_contains && schema.defines("minContains") && + schema.at("minContains").is_integer()) { + const auto value{schema.at("minContains").to_integer()}; + if (value == 0) { + constraints.emplace_back("0 or more matching items"); + } else { + constraints.emplace_back(">= " + std::to_string(value) + + " matching items"); + } + } + if (!has_trivial_contains && schema.defines("maxContains") && + schema.at("maxContains").is_integer()) { + constraints.emplace_back( + "<= " + std::to_string(schema.at("maxContains").to_integer()) + + " matching items"); + } + + // Flat contains: inline the contains schema constraints with a prefix + if (schema.defines("contains") && schema.at("contains").is_object()) { + const auto &contains_schema{schema.at("contains")}; + const auto is_branching{ + contains_schema.defines("anyOf") || contains_schema.defines("oneOf") || + contains_schema.defines("allOf") || contains_schema.defines("not")}; + if (!is_branching) { + const auto inner{constraints_of(contains_schema)}; + for (const auto &constraint : inner) { + constraints.emplace_back("contains " + constraint); + } + } + } + + if (schema.defines("propertyNames") && + schema.at("propertyNames").is_object()) { + const auto &names_schema{schema.at("propertyNames")}; + const auto is_branching{ + names_schema.defines("anyOf") || names_schema.defines("oneOf") || + names_schema.defines("allOf") || names_schema.defines("not")}; + if (!is_branching) { + const auto inner{constraints_of(names_schema)}; + for (const auto &constraint : inner) { + constraints.emplace_back("keys " + constraint); + } + } + } + + if (schema.defines("contentSchema") && + schema.at("contentSchema").is_object()) { + const auto &content_schema{schema.at("contentSchema")}; + const auto is_branching{ + content_schema.defines("anyOf") || content_schema.defines("oneOf") || + content_schema.defines("allOf") || content_schema.defines("not")}; + if (!is_branching) { + const auto inner{constraints_of(content_schema)}; + for (const auto &constraint : inner) { + constraints.emplace_back("decoded " + constraint); + } + } + } + + if (schema.defines("not") && schema.at("not").is_object()) { + const auto ¬_schema{schema.at("not")}; + const auto is_branching{ + not_schema.defines("anyOf") || not_schema.defines("oneOf") || + not_schema.defines("allOf") || not_schema.defines("not")}; + if (!is_branching) { + const auto inner{constraints_of(not_schema)}; + for (const auto &constraint : inner) { + constraints.emplace_back("must NOT match " + constraint); + } + } + } + + return constraints; +} + +auto is_required_property(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON::String &property) + -> bool { + if (!schema.is_object() || !schema.defines("required") || + !schema.at("required").is_array()) { + return false; + } + for (const auto &item : schema.at("required").as_array()) { + if (item.is_string() && item.to_string() == property) { + return true; + } + } + return false; +} + +auto walk_schema(const sourcemeta::core::JSON &schema, const bool include_root, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, VisitedSchemas &visited, + std::size_t &next_identifier) -> Documentation; + +auto walk_all_of(const sourcemeta::core::JSON &schema, + std::vector &children, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, VisitedSchemas &visited, + std::size_t &next_identifier) -> void; + +auto walk_if_then_else(const sourcemeta::core::JSON &schema, + std::vector &children, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, + VisitedSchemas &visited, std::size_t &next_identifier) + -> void; + +auto walk_additional_properties( + const sourcemeta::core::JSON &schema, + const std::vector &base_path, + std::vector &rows, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, VisitedSchemas &visited, + std::size_t &next_identifier) -> void; + +auto walk_unevaluated_properties( + const sourcemeta::core::JSON &schema, + const std::vector &base_path, + std::vector &rows, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, VisitedSchemas &visited, + std::size_t &next_identifier) -> void; + +auto walk_unevaluated_items( + const sourcemeta::core::JSON &schema, + const std::vector &base_path, + std::vector &rows, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, VisitedSchemas &visited, + std::size_t &next_identifier) -> void; + +auto walk_pattern_properties( + const sourcemeta::core::JSON &schema, + const std::vector &base_path, + std::vector &rows, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, VisitedSchemas &visited, + std::size_t &next_identifier) -> void; + +auto resolve_ref(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, + const VisitedSchemas &visited) + -> const sourcemeta::core::JSON & { + if (schema.is_object() && schema.defines("$ref") && + schema.at("$ref").is_string()) { + const auto target{ + resolve_destination(schema.at("$ref").to_string(), frame)}; + if (target.has_value()) { + const auto &target_schema{ + sourcemeta::core::get(root, target->get().pointer)}; + if (visited.find(&target_schema) != visited.end()) { + return schema; // NOLINT(bugprone-return-const-ref-from-parameter) + } + return target_schema; + } + } + return schema; // NOLINT(bugprone-return-const-ref-from-parameter) +} + +auto emit_row(const sourcemeta::core::JSON &schema, + std::vector path, + std::vector &rows, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, const VisitedSchemas &visited, + std::size_t &next_identifier) -> void { + Documentation::Row row; + row.identifier = next_identifier++; + row.path = std::move(path); + row.modifiers = modifiers_of(schema); + row.type.expression = type_expression_of(schema, frame, root, visited); + row.type.badges = badges_of(schema); + row.constraints = constraints_of(schema); + row.notes = notes_of(schema); + rows.push_back(std::move(row)); +} + +auto walk_properties(const sourcemeta::core::JSON &schema, + const std::vector &base_path, + std::vector &rows, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, + VisitedSchemas &visited, std::size_t &next_identifier) + -> void { + if (!schema.is_object() || !schema.defines("properties") || + !schema.at("properties").is_object()) { + return; + } + + for (const auto &entry : schema.at("properties").as_object()) { + const auto &resolved{resolve_ref(entry.second, frame, root, visited)}; + auto path{base_path}; + path.push_back( + {.type = Documentation::PathType::Literal, .value = entry.first}); + Documentation::Row row; + row.identifier = next_identifier++; + row.path = path; + row.modifiers = modifiers_of(resolved); + row.type.expression = type_expression_of(resolved, frame, root, visited); + row.type.badges = badges_of(resolved); + row.constraints = constraints_of(resolved); + row.notes = notes_of(resolved); + row.required = is_required_property(schema, entry.first); + rows.push_back(std::move(row)); + + if (resolved.is_object() && resolved.defines("type") && + resolved.at("type").is_string() && + resolved.at("type").to_string() == "object") { + visited.emplace(&resolved, rows.back().identifier); + walk_properties(resolved, path, rows, frame, root, visited, + next_identifier); + walk_pattern_properties(resolved, path, rows, frame, root, visited, + next_identifier); + walk_additional_properties(resolved, path, rows, frame, root, visited, + next_identifier); + walk_unevaluated_properties(resolved, path, rows, frame, root, visited, + next_identifier); + visited.erase(&resolved); + } + } +} + +auto walk_additional_properties( + const sourcemeta::core::JSON &schema, + const std::vector &base_path, + std::vector &rows, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, VisitedSchemas &visited, + std::size_t &next_identifier) -> void { + if (!schema.is_object() || !schema.defines("additionalProperties") || + !schema.at("additionalProperties").is_object()) { + return; + } + + auto path{base_path}; + path.push_back({.type = Documentation::PathType::Wildcard, .value = "*"}); + emit_row(schema.at("additionalProperties"), std::move(path), rows, frame, + root, visited, next_identifier); +} + +auto walk_unevaluated_properties( + const sourcemeta::core::JSON &schema, + const std::vector &base_path, + std::vector &rows, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, VisitedSchemas &visited, + std::size_t &next_identifier) -> void { + if (!schema.is_object() || !schema.defines("unevaluatedProperties") || + !schema.at("unevaluatedProperties").is_object()) { + return; + } + + auto path{base_path}; + path.push_back({.type = Documentation::PathType::Wildcard, .value = "*"}); + emit_row(schema.at("unevaluatedProperties"), std::move(path), rows, frame, + root, visited, next_identifier); +} + +auto walk_unevaluated_items( + const sourcemeta::core::JSON &schema, + const std::vector &base_path, + std::vector &rows, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, VisitedSchemas &visited, + std::size_t &next_identifier) -> void { + if (!schema.is_object() || !schema.defines("unevaluatedItems") || + !schema.at("unevaluatedItems").is_object()) { + return; + } + + if (schema.defines("prefixItems")) { + return; + } + + auto path{base_path}; + path.push_back({.type = Documentation::PathType::Wildcard, .value = "*"}); + emit_row(schema.at("unevaluatedItems"), std::move(path), rows, frame, root, + visited, next_identifier); +} + +auto walk_pattern_properties( + const sourcemeta::core::JSON &schema, + const std::vector &base_path, + std::vector &rows, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, VisitedSchemas &visited, + std::size_t &next_identifier) -> void { + if (!schema.is_object() || !schema.defines("patternProperties") || + !schema.at("patternProperties").is_object()) { + return; + } + + for (const auto &entry : schema.at("patternProperties").as_object()) { + auto path{base_path}; + path.push_back( + {.type = Documentation::PathType::Pattern, .value = entry.first}); + emit_row(entry.second, std::move(path), rows, frame, root, visited, + next_identifier); + } +} + +auto is_complex_schema(const sourcemeta::core::JSON &schema) -> bool { + if (!schema.is_object()) { + return false; + } + return schema.defines("properties") || schema.defines("anyOf") || + schema.defines("oneOf") || schema.defines("allOf") || + schema.defines("not") || schema.defines("if") || + schema.defines("prefixItems") || schema.defines("contains") || + schema.defines("patternProperties") || + schema.defines("additionalProperties") || + schema.defines("propertyNames") || schema.defines("contentSchema"); +} + +auto walk_prefix_items(const sourcemeta::core::JSON &schema, + const std::vector &base_path, + std::vector &rows, + std::vector &children, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, + VisitedSchemas &visited, std::size_t &next_identifier) + -> void { + if (!schema.is_object() || !schema.defines("prefixItems") || + !schema.at("prefixItems").is_array()) { + return; + } + + std::size_t min_items{0}; + if (schema.defines("minItems") && schema.at("minItems").is_integer() && + schema.at("minItems").to_integer() > 0) { + min_items = static_cast(schema.at("minItems").to_integer()); + } + + std::size_t index{0}; + for (const auto &item : schema.at("prefixItems").as_array()) { + if (is_complex_schema(item)) { + Documentation::Section section; + section.label = "Array item " + std::to_string(index); + section.children.push_back( + walk_schema(item, true, frame, root, visited, next_identifier)); + children.push_back(std::move(section)); + } else { + auto path{base_path}; + path.push_back({.type = Documentation::PathType::Literal, + .value = std::to_string(index)}); + Documentation::Row row; + row.identifier = next_identifier++; + row.path = std::move(path); + row.modifiers = modifiers_of(item); + row.type.expression = type_expression_of(item, frame, root, visited); + row.type.badges = badges_of(item); + row.constraints = constraints_of(item); + row.notes = notes_of(item); + row.required = index < min_items; + rows.push_back(std::move(row)); + } + ++index; + } +} + +auto walk_any_of(const sourcemeta::core::JSON &schema, + std::vector &children, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, VisitedSchemas &visited, + std::size_t &next_identifier) -> void { + if (!schema.is_object() || !schema.defines("anyOf") || + !schema.at("anyOf").is_array()) { + return; + } + + Documentation::Section section; + section.label = "Any of"; + for (const auto &branch : schema.at("anyOf").as_array()) { + section.children.push_back( + walk_schema(branch, false, frame, root, visited, next_identifier)); + } + children.push_back(std::move(section)); +} + +auto walk_one_of(const sourcemeta::core::JSON &schema, + std::vector &children, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, VisitedSchemas &visited, + std::size_t &next_identifier) -> void { + if (!schema.is_object() || !schema.defines("oneOf") || + !schema.at("oneOf").is_array()) { + return; + } + + Documentation::Section section; + section.label = "One of"; + for (const auto &branch : schema.at("oneOf").as_array()) { + section.children.push_back( + walk_schema(branch, false, frame, root, visited, next_identifier)); + } + children.push_back(std::move(section)); +} + +auto walk_all_of(const sourcemeta::core::JSON &schema, + std::vector &children, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, VisitedSchemas &visited, + std::size_t &next_identifier) -> void { + if (!schema.is_object() || !schema.defines("allOf") || + !schema.at("allOf").is_array()) { + return; + } + + Documentation::Section section; + section.label = "All of"; + for (const auto &branch : schema.at("allOf").as_array()) { + section.children.push_back( + walk_schema(branch, false, frame, root, visited, next_identifier)); + } + children.push_back(std::move(section)); +} + +auto walk_if_then_else(const sourcemeta::core::JSON &schema, + std::vector &children, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, + VisitedSchemas &visited, std::size_t &next_identifier) + -> void { + if (!schema.is_object() || !schema.defines("if") || !schema.defines("then") || + !schema.defines("else")) { + return; + } + + Documentation::Section if_section; + if_section.label = "If"; + if_section.children.push_back(walk_schema(schema.at("if"), false, frame, root, + visited, next_identifier)); + children.push_back(std::move(if_section)); + + Documentation::Section then_section; + then_section.label = "Then"; + then_section.children.push_back(walk_schema(schema.at("then"), false, frame, + root, visited, next_identifier)); + children.push_back(std::move(then_section)); + + Documentation::Section else_section; + else_section.label = "Else"; + else_section.children.push_back(walk_schema(schema.at("else"), false, frame, + root, visited, next_identifier)); + children.push_back(std::move(else_section)); +} + +auto check_unsupported_keywords(const sourcemeta::core::JSON &schema) -> void { + if (!schema.is_object()) { + return; + } + + static const std::array unsupported{ + "dependentSchemas", "dependentRequired"}; + for (const auto &keyword : unsupported) { + if (schema.defines(keyword)) { + throw DocumentationUnknownKeywordError{keyword}; + } + } +} + +auto walk_schema(const sourcemeta::core::JSON &schema, const bool include_root, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, VisitedSchemas &visited, + std::size_t &next_identifier) -> Documentation { + if (schema.is_object() && schema.defines("$ref") && + schema.at("$ref").is_string()) { + const auto target{ + resolve_destination(schema.at("$ref").to_string(), frame)}; + if (target.has_value()) { + const auto &target_schema{ + sourcemeta::core::get(root, target->get().pointer)}; + const auto visited_entry{visited.find(&target_schema)}; + if (visited_entry != visited.end()) { + Documentation documentation; + Documentation::Row row; + if (include_root) { + row.path = { + {.type = Documentation::PathType::Synthetic, .value = "root"}}; + } + Documentation::Type::Expression::RecursiveReference reference; + reference.identifier = visited_entry->second; + row.type.expression.value = reference; + documentation.rows.push_back(std::move(row)); + return documentation; + } + visited.emplace(&target_schema, next_identifier); + auto result{walk_schema(target_schema, include_root, frame, root, visited, + next_identifier)}; + visited.erase(&target_schema); + return result; + } + } + + check_unsupported_keywords(schema); + Documentation documentation; + documentation.identifier = next_identifier++; + + if (schema.is_object() && schema.defines("$dynamicAnchor") && + schema.at("$dynamicAnchor").is_string()) { + documentation.dynamic_anchor = schema.at("$dynamicAnchor").to_string(); + } + + if (include_root) { + emit_row(schema, + {{.type = Documentation::PathType::Synthetic, .value = "root"}}, + documentation.rows, frame, root, visited, next_identifier); + visited.emplace(&schema, documentation.rows.back().identifier); + } + + if (!schema.is_object()) { + if (!include_root) { + emit_row(schema, {}, documentation.rows, frame, root, visited, + next_identifier); + } + return documentation; + } + + const std::vector empty_path; + walk_properties(schema, empty_path, documentation.rows, frame, root, visited, + next_identifier); + walk_pattern_properties(schema, empty_path, documentation.rows, frame, root, + visited, next_identifier); + walk_additional_properties(schema, empty_path, documentation.rows, frame, + root, visited, next_identifier); + walk_prefix_items(schema, empty_path, documentation.rows, + documentation.children, frame, root, visited, + next_identifier); + walk_any_of(schema, documentation.children, frame, root, visited, + next_identifier); + walk_one_of(schema, documentation.children, frame, root, visited, + next_identifier); + walk_all_of(schema, documentation.children, frame, root, visited, + next_identifier); + walk_if_then_else(schema, documentation.children, frame, root, visited, + next_identifier); + walk_unevaluated_properties(schema, empty_path, documentation.rows, frame, + root, visited, next_identifier); + walk_unevaluated_items(schema, empty_path, documentation.rows, frame, root, + visited, next_identifier); + + if (schema.is_object() && schema.defines("contains") && + schema.at("contains").is_object()) { + const auto &contains_schema{schema.at("contains")}; + const auto is_branching{ + contains_schema.defines("anyOf") || contains_schema.defines("oneOf") || + contains_schema.defines("allOf") || contains_schema.defines("not")}; + if (is_branching) { + Documentation::Section section; + section.label = "Contains"; + Documentation table; + emit_row(contains_schema, + {{.type = Documentation::PathType::Synthetic, + .value = "matching item"}}, + table.rows, frame, root, visited, next_identifier); + walk_any_of(contains_schema, table.children, frame, root, visited, + next_identifier); + walk_one_of(contains_schema, table.children, frame, root, visited, + next_identifier); + walk_all_of(contains_schema, table.children, frame, root, visited, + next_identifier); + section.children.push_back(std::move(table)); + documentation.children.push_back(std::move(section)); + } + } + + if (schema.is_object() && schema.defines("contentSchema") && + schema.at("contentSchema").is_object()) { + const auto &content_schema{schema.at("contentSchema")}; + const auto is_branching{ + content_schema.defines("anyOf") || content_schema.defines("oneOf") || + content_schema.defines("allOf") || content_schema.defines("not")}; + if (is_branching) { + Documentation::Section section; + section.label = "Decoded content"; + Documentation table; + const std::vector decoded_path{ + {.type = Documentation::PathType::Synthetic, .value = "decoded"}}; + emit_row(content_schema, decoded_path, table.rows, frame, root, visited, + next_identifier); + walk_properties(content_schema, decoded_path, table.rows, frame, root, + visited, next_identifier); + walk_any_of(content_schema, table.children, frame, root, visited, + next_identifier); + walk_one_of(content_schema, table.children, frame, root, visited, + next_identifier); + walk_all_of(content_schema, table.children, frame, root, visited, + next_identifier); + section.children.push_back(std::move(table)); + documentation.children.push_back(std::move(section)); + } + } + + if (schema.is_object() && schema.defines("propertyNames") && + schema.at("propertyNames").is_object()) { + const auto &names_schema{schema.at("propertyNames")}; + const auto is_branching{ + names_schema.defines("anyOf") || names_schema.defines("oneOf") || + names_schema.defines("allOf") || names_schema.defines("not")}; + if (is_branching) { + Documentation::Section section; + section.label = "Property names"; + Documentation table; + emit_row(names_schema, + {{.type = Documentation::PathType::Synthetic, .value = "key"}}, + table.rows, frame, root, visited, next_identifier); + walk_any_of(names_schema, table.children, frame, root, visited, + next_identifier); + walk_one_of(names_schema, table.children, frame, root, visited, + next_identifier); + walk_all_of(names_schema, table.children, frame, root, visited, + next_identifier); + section.children.push_back(std::move(table)); + documentation.children.push_back(std::move(section)); + } + } + + if (schema.is_object() && schema.defines("not") && + schema.at("not").is_object()) { + const auto ¬_schema{schema.at("not")}; + const auto is_branching{ + not_schema.defines("anyOf") || not_schema.defines("oneOf") || + not_schema.defines("allOf") || not_schema.defines("not")}; + if (is_branching) { + Documentation::Section section; + section.label = "Must NOT match"; + Documentation table; + emit_row(not_schema, + {{.type = Documentation::PathType::Synthetic, .value = "value"}}, + table.rows, frame, root, visited, next_identifier); + walk_any_of(not_schema, table.children, frame, root, visited, + next_identifier); + walk_one_of(not_schema, table.children, frame, root, visited, + next_identifier); + walk_all_of(not_schema, table.children, frame, root, visited, + next_identifier); + section.children.push_back(std::move(table)); + documentation.children.push_back(std::move(section)); + } + } + + if (schema.is_object() && schema.defines("not") && + schema.at("not").is_boolean()) { + Documentation::Section section; + section.label = "Must NOT match"; + Documentation table; + emit_row(schema.at("not"), + {{.type = Documentation::PathType::Synthetic, .value = "value"}}, + table.rows, frame, root, visited, next_identifier); + section.children.push_back(std::move(table)); + documentation.children.push_back(std::move(section)); + } + + // A branch with no properties and no sub-applicators still has something + // to say (constraints, type, notes). Emit a single row so it is not lost. + if (!include_root && documentation.rows.empty() && + documentation.children.empty() && schema.is_object()) { + emit_row(schema, {}, documentation.rows, frame, root, visited, + next_identifier); + } + + assert(!documentation.rows.empty() || !documentation.children.empty()); + + return documentation; +} + +} // namespace + +auto to_documentation(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &resolver) + -> Documentation { + // Canonicalize the schema for easier analysis + sourcemeta::blaze::SchemaTransformer canonicalizer; + sourcemeta::blaze::add(canonicalizer, + sourcemeta::blaze::AlterSchemaMode::Canonicalizer); + sourcemeta::core::JSON canonical{schema}; + [[maybe_unused]] const auto canonicalized{canonicalizer.apply( + canonical, walker, resolver, + [](const auto &, const auto, const auto, const auto &, + [[maybe_unused]] const auto applied) { assert(applied); })}; + assert(canonicalized.first); + + // Frame the canonicalized schema with reference information + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + frame.analyse(canonical, walker, resolver); + + VisitedSchemas visited; + std::size_t next_identifier{0}; + return walk_schema(canonical, true, frame, canonical, visited, + next_identifier); +} + +} // namespace sourcemeta::blaze diff --git a/src/documentation/include/sourcemeta/blaze/documentation.h b/src/documentation/include/sourcemeta/blaze/documentation.h new file mode 100644 index 000000000..4313f98de --- /dev/null +++ b/src/documentation/include/sourcemeta/blaze/documentation.h @@ -0,0 +1,212 @@ +#ifndef SOURCEMETA_BLAZE_DOCUMENTATION_H_ +#define SOURCEMETA_BLAZE_DOCUMENTATION_H_ + +/// @defgroup documentation Documentation +/// @brief Generate human-readable documentation from a JSON Schema +/// +/// This functionality is included as follows: +/// +/// ```cpp +/// #include +/// ``` + +#ifndef SOURCEMETA_BLAZE_DOCUMENTATION_EXPORT +#include +#endif + +// NOLINTBEGIN(misc-include-cleaner) +#include +// NOLINTEND(misc-include-cleaner) + +#include +#include + +#include // std::uint8_t +#include // std::optional +#include // std::variant +#include // std::vector + +namespace sourcemeta::blaze { + +// Exporting symbols that depends on the standard C++ library is considered +// safe. +// https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4275?view=msvc-170&redirectedfrom=MSDN +#if defined(_MSC_VER) +#pragma warning(disable : 4251 4275) +#endif + +/// @ingroup documentation +/// A documentation table that describes a JSON Schema +struct SOURCEMETA_BLAZE_DOCUMENTATION_EXPORT Documentation { + struct Row; + struct Section; + + /// A modifier badge that appears below the pointer in the path column + enum class Modifier : std::uint8_t { ReadOnly, WriteOnly, Deprecated }; + + /// A type-column chip such as a `format`, `contentEncoding`, or + /// `contentMediaType` annotation + struct Badge { + enum class Kind : std::uint8_t { Format, Encoding, Mime }; + Kind kind; + sourcemeta::core::JSON::String value; + }; + + /// The notes-column content for a row. If every member is empty, the cell + /// renders empty + struct Notes { + std::optional title; + std::optional description; + std::optional default_value; + std::vector examples; + }; + + /// The type-column content for a row: a type expression and zero or more + /// chips appended after it + struct Type { + /// A type expression as it appears in the type column. The active + /// alternative of the inner variant identifies the kind + struct Expression { + /// An object type expression + struct Object {}; + + /// A primitive type expression. Note that `boolean` and `null` are + /// intentionally absent: the canonicalizer rewrites `type: boolean` + /// as `enum: [false, true]` and `type: null` as `enum: [null]`, so + /// they always reach the documentation as `Enumeration` expressions + enum class Primitive : std::uint8_t { String, Integer, Number }; + + /// An array type expression. The vector is empty when the array has + /// no `items` constraint, and otherwise holds exactly one element + /// describing the homogeneous item type + using Array = std::vector; + + /// A tuple type expression. The `items` member holds one entry per + /// `prefixItems` index. The `additional` member is empty when the + /// tuple has no open tail, and otherwise holds exactly one element + /// describing the open-tail type + struct Tuple { + std::vector items; + std::vector additional; + }; + + /// An enumeration type expression. The `values` member holds the + /// visible portion (the first ten by default) and the `overflow` + /// member holds the rest, to be rendered inside a `
` element + struct Enumeration { + std::vector values; + std::vector overflow; + }; + + /// An external `$ref` type expression carrying the target URL verbatim + struct ExternalReference { + sourcemeta::core::JSON::String url; + }; + + /// A recursive `$ref` type expression carrying the identifier of the + /// target table in the same documentation tree + struct RecursiveReference { + std::size_t identifier; + }; + + /// A `$dynamicRef` type expression carrying the target anchor name + struct DynamicReference { + sourcemeta::core::JSON::String anchor; + }; + + /// A type expression meaning any value is valid (from a boolean `true` + /// schema) + struct Any {}; + + /// A type expression meaning no value is valid (from a boolean `false` + /// schema) + struct Never {}; + + std::variant + value; + }; + + Expression expression; + std::vector badges; + }; + + /// The kind of a path segment in a documentation row + enum class PathType : std::uint8_t { Literal, Pattern, Wildcard, Synthetic }; + + /// A single segment of a row's path. The `type` member identifies what + /// kind of segment it is, and the `value` member holds the segment text + struct PathSegment { + PathType type; + sourcemeta::core::JSON::String value; + auto operator==(const PathSegment &other) const -> bool { + return this->type == other.type && this->value == other.value; + } + }; + + /// A row in the documentation table. The `children` member holds variants + /// rows that render immediately after this row. The `required` member is + /// `true` when the row's position is required, `false` when it is not, and + /// empty when the required column does not apply + struct Row { + std::size_t identifier{0}; + std::vector path; + std::vector modifiers; + Type type; + std::optional required; + std::vector constraints; + Notes notes; + std::vector
children; + }; + + /// A sub-schema section emitted as a variants row. The `position` member is + /// the JSON Pointer of the operator within the parent schema, or null for + /// the top-level. Each entry in `children` is one branch rendered as a + /// variant card + struct Section { + sourcemeta::core::JSON::String label; + std::optional position; + std::vector children; + }; + + std::size_t identifier{0}; + std::optional title; + std::optional dynamic_anchor; + std::vector rows; + std::vector
children; +}; + +/// @ingroup documentation +/// +/// This function takes an input JSON Schema and produces a documentation +/// representation of it. For example: +/// +/// ```cpp +/// #include +/// +/// #include +/// #include +/// +/// const sourcemeta::core::JSON schema = +/// sourcemeta::core::parse_json(R"JSON({ +/// "$schema": "https://json-schema.org/draft/2020-12/schema", +/// "type": "string" +/// })JSON"); +/// +/// const auto documentation{sourcemeta::blaze::to_documentation( +/// schema, sourcemeta::core::schema_walker, +/// sourcemeta::core::schema_resolver)}; +/// ``` +auto SOURCEMETA_BLAZE_DOCUMENTATION_EXPORT to_documentation( + const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &resolver) -> Documentation; + +#if defined(_MSC_VER) +#pragma warning(default : 4251 4275) +#endif + +} // namespace sourcemeta::blaze + +#endif diff --git a/src/documentation/include/sourcemeta/blaze/documentation_error.h b/src/documentation/include/sourcemeta/blaze/documentation_error.h new file mode 100644 index 000000000..bc17c6af1 --- /dev/null +++ b/src/documentation/include/sourcemeta/blaze/documentation_error.h @@ -0,0 +1,48 @@ +#ifndef SOURCEMETA_BLAZE_DOCUMENTATION_ERROR_H_ +#define SOURCEMETA_BLAZE_DOCUMENTATION_ERROR_H_ + +#ifndef SOURCEMETA_BLAZE_DOCUMENTATION_EXPORT +#include +#endif + +#include // std::exception +#include // std::string +#include // std::string_view + +namespace sourcemeta::blaze { + +// Exporting symbols that depends on the standard C++ library is considered +// safe. +// https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4275?view=msvc-170&redirectedfrom=MSDN +#if defined(_MSC_VER) +#pragma warning(disable : 4251 4275) +#endif + +/// @ingroup documentation +/// An error thrown when the documentation generator encounters a JSON Schema +/// keyword that it does not support +class SOURCEMETA_BLAZE_DOCUMENTATION_EXPORT DocumentationUnknownKeywordError + : public std::exception { +public: + DocumentationUnknownKeywordError(const std::string_view keyword) + : keyword_{keyword} {} + + [[nodiscard]] auto what() const noexcept -> const char * override { + return "The documentation generator encountered an unsupported keyword"; + } + + [[nodiscard]] auto keyword() const noexcept -> const std::string & { + return this->keyword_; + } + +private: + std::string keyword_; +}; + +#if defined(_MSC_VER) +#pragma warning(default : 4251 4275) +#endif + +} // namespace sourcemeta::blaze + +#endif diff --git a/test/documentation/CMakeLists.txt b/test/documentation/CMakeLists.txt new file mode 100644 index 000000000..99e761859 --- /dev/null +++ b/test/documentation/CMakeLists.txt @@ -0,0 +1,12 @@ +sourcemeta_googletest(NAMESPACE sourcemeta PROJECT blaze NAME documentation + FOLDER "Blaze/Documentation" + SOURCES + documentation_test_utils.h + documentation_2020_12_test.cc) + +target_link_libraries(sourcemeta_blaze_documentation_unit + PRIVATE sourcemeta::core::json) +target_link_libraries(sourcemeta_blaze_documentation_unit + PRIVATE sourcemeta::core::jsonschema) +target_link_libraries(sourcemeta_blaze_documentation_unit + PRIVATE sourcemeta::blaze::documentation) diff --git a/test/documentation/documentation_2020_12_test.cc b/test/documentation/documentation_2020_12_test.cc new file mode 100644 index 000000000..41791ba5b --- /dev/null +++ b/test/documentation/documentation_2020_12_test.cc @@ -0,0 +1,2262 @@ +#include + +#include + +#include +#include + +#include "documentation_test_utils.h" + +TEST(Documentation_2020_12, type_string) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 0); + EXPECT_ROOT_ROW_PRIMITIVE( + 1, documentation, 0, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String); +} + +TEST(Documentation_2020_12, object_with_const_and_anyof) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "title": "Pet", + "properties": { + "kind": { "const": "cat" } + }, + "anyOf": [ + { "properties": { "indoor": { "type": "boolean" } } }, + { "properties": { "outdoor": { "type": "boolean" } } } + ] + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 1); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + + EXPECT_SECTION(documentation, 0, "All of", 2); + const auto &all_of{documentation.children.front()}; + + const auto &applicator_branch{all_of.children.at(0)}; + EXPECT_TABLE_SIZE(applicator_branch, 2, 0, 1); + EXPECT_EQ(applicator_branch.children.front().label, "Any of"); + EXPECT_EQ(applicator_branch.children.front().children.size(), 7); + + const auto &null_branch{applicator_branch.children.front().children.at(0)}; + EXPECT_TABLE_SIZE(null_branch, 3, 1, 0); + EXPECT_WILDCARD_ROW_ENUM(4, null_branch, 0, PATH(), 1, 0); + const auto &bool_branch{applicator_branch.children.front().children.at(1)}; + EXPECT_TABLE_SIZE(bool_branch, 5, 1, 0); + EXPECT_WILDCARD_ROW_ENUM(6, bool_branch, 0, PATH(), 2, 0); + const auto &indoor_branch{applicator_branch.children.front().children.at(2)}; + EXPECT_TABLE_SIZE(indoor_branch, 7, 1, 0); + EXPECT_ROW_ENUM_BOOLEANS(8, indoor_branch, 0, PATH(LITERAL("indoor")), false, + false, true); + const auto &array_branch{applicator_branch.children.front().children.at(3)}; + EXPECT_TABLE_SIZE(array_branch, 9, 1, 0); + EXPECT_WILDCARD_ROW_ARRAY(10, array_branch, 0, PATH()); + const auto &string_branch{applicator_branch.children.front().children.at(4)}; + EXPECT_TABLE_SIZE(string_branch, 11, 1, 0); + EXPECT_WILDCARD_ROW_PRIMITIVE( + 12, string_branch, 0, PATH(), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String); + const auto &number_branch{applicator_branch.children.front().children.at(5)}; + EXPECT_TABLE_SIZE(number_branch, 13, 1, 0); + EXPECT_WILDCARD_ROW_PRIMITIVE( + 14, number_branch, 0, PATH(), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Number); + const auto &outdoor_branch{applicator_branch.children.front().children.at(6)}; + EXPECT_TABLE_SIZE(outdoor_branch, 15, 1, 0); + EXPECT_ROW_ENUM_BOOLEANS(16, outdoor_branch, 0, PATH(LITERAL("outdoor")), + false, false, true); + + const auto &typed_branch{all_of.children.at(1)}; + EXPECT_TABLE_SIZE(typed_branch, 17, 1, 0); + EXPECT_ROW_ENUM_STRINGS(18, typed_branch, 0, PATH(LITERAL("kind")), false, + "cat"); +} + +TEST(Documentation_2020_12, object_required_with_annotations) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "description": "The user email address" + }, + "age": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ "email" ] + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 3, 0); + EXPECT_ROOT_ROW_OBJECT_WITH_CONSTRAINTS(1, documentation, 0, + ">= 1 properties"); + EXPECT_ROW_PRIMITIVE_WITH_BADGE_AND_DESCRIPTION( + 2, documentation, 1, PATH(LITERAL("email")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + true, sourcemeta::blaze::Documentation::Badge::Kind::Format, "email", + "The user email address"); + EXPECT_ROW_PRIMITIVE_WITH_CONSTRAINTS( + 3, documentation, 2, PATH(LITERAL("age")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Integer, + false, ">= 0"); +} + +TEST(Documentation_2020_12, if_then_else_as_allof) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "kind": { "type": "string" } + }, + "if": { + "properties": { "kind": { "const": "dog" } } + }, + "then": { + "properties": { "breed": { "type": "string" } } + }, + "else": { + "properties": { "color": { "type": "string" } } + } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 1); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + + EXPECT_SECTION(documentation, 0, "All of", 2); + const auto &all_of{documentation.children.front()}; + + const auto &conditional_branch{all_of.children.at(0)}; + EXPECT_TABLE_SIZE(conditional_branch, 2, 0, 3); + EXPECT_EQ(conditional_branch.children.at(0).label, "If"); + EXPECT_EQ(conditional_branch.children.at(0).children.size(), 1); + const auto &if_table{conditional_branch.children.at(0).children.front()}; + EXPECT_TABLE_SIZE(if_table, 3, 0, 1); + EXPECT_EQ(if_table.children.front().label, "Any of"); + EXPECT_EQ(if_table.children.front().children.size(), 6); + EXPECT_EQ(conditional_branch.children.at(1).label, "Then"); + EXPECT_EQ(conditional_branch.children.at(1).children.size(), 1); + const auto &then_table{conditional_branch.children.at(1).children.front()}; + EXPECT_TABLE_SIZE(then_table, 16, 0, 1); + EXPECT_EQ(then_table.children.front().label, "Any of"); + EXPECT_EQ(then_table.children.front().children.size(), 6); + EXPECT_EQ(conditional_branch.children.at(2).label, "Else"); + EXPECT_EQ(conditional_branch.children.at(2).children.size(), 1); + const auto &else_table{conditional_branch.children.at(2).children.front()}; + EXPECT_TABLE_SIZE(else_table, 29, 0, 1); + EXPECT_EQ(else_table.children.front().label, "Any of"); + EXPECT_EQ(else_table.children.front().children.size(), 6); + + const auto &typed_branch{all_of.children.at(1)}; + EXPECT_TABLE_SIZE(typed_branch, 42, 1, 0); + EXPECT_ROW_PRIMITIVE( + 43, typed_branch, 0, PATH(LITERAL("kind")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + false); +} + +TEST(Documentation_2020_12, allof_object) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "allOf": [ + { + "properties": { "name": { "type": "string" } }, + "required": [ "name" ] + }, + { + "properties": { "age": { "type": "integer" } }, + "required": [ "age" ] + } + ] + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 1); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + + EXPECT_SECTION(documentation, 0, "All of", 3); + const auto &all_of{documentation.children.front()}; + + const auto &name_anyof_branch{all_of.children.at(0)}; + EXPECT_TABLE_SIZE(name_anyof_branch, 2, 0, 1); + EXPECT_EQ(name_anyof_branch.children.front().label, "Any of"); + EXPECT_EQ(name_anyof_branch.children.front().children.size(), 6); + const auto &name_null{name_anyof_branch.children.front().children.at(0)}; + EXPECT_TABLE_SIZE(name_null, 3, 1, 0); + EXPECT_WILDCARD_ROW_ENUM(4, name_null, 0, PATH(), 1, 0); + const auto &name_bool{name_anyof_branch.children.front().children.at(1)}; + EXPECT_TABLE_SIZE(name_bool, 5, 1, 0); + EXPECT_WILDCARD_ROW_ENUM(6, name_bool, 0, PATH(), 2, 0); + const auto &name_object{name_anyof_branch.children.front().children.at(2)}; + EXPECT_TABLE_SIZE(name_object, 7, 1, 0); + EXPECT_ROW_PRIMITIVE( + 8, name_object, 0, PATH(LITERAL("name")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + true); + const auto &name_array{name_anyof_branch.children.front().children.at(3)}; + EXPECT_TABLE_SIZE(name_array, 9, 1, 0); + EXPECT_WILDCARD_ROW_ARRAY(10, name_array, 0, PATH()); + const auto &name_string{name_anyof_branch.children.front().children.at(4)}; + EXPECT_TABLE_SIZE(name_string, 11, 1, 0); + EXPECT_WILDCARD_ROW_PRIMITIVE( + 12, name_string, 0, PATH(), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String); + const auto &name_number{name_anyof_branch.children.front().children.at(5)}; + EXPECT_TABLE_SIZE(name_number, 13, 1, 0); + EXPECT_WILDCARD_ROW_PRIMITIVE( + 14, name_number, 0, PATH(), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Number); + + const auto &age_anyof_branch{all_of.children.at(1)}; + EXPECT_TABLE_SIZE(age_anyof_branch, 15, 0, 1); + EXPECT_EQ(age_anyof_branch.children.front().label, "Any of"); + EXPECT_EQ(age_anyof_branch.children.front().children.size(), 6); + const auto &age_null{age_anyof_branch.children.front().children.at(0)}; + EXPECT_TABLE_SIZE(age_null, 16, 1, 0); + EXPECT_WILDCARD_ROW_ENUM(17, age_null, 0, PATH(), 1, 0); + const auto &age_bool{age_anyof_branch.children.front().children.at(1)}; + EXPECT_TABLE_SIZE(age_bool, 18, 1, 0); + EXPECT_WILDCARD_ROW_ENUM(19, age_bool, 0, PATH(), 2, 0); + const auto &age_object{age_anyof_branch.children.front().children.at(2)}; + EXPECT_TABLE_SIZE(age_object, 20, 1, 0); + EXPECT_ROW_PRIMITIVE( + 21, age_object, 0, PATH(LITERAL("age")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Integer, + true); + const auto &age_array{age_anyof_branch.children.front().children.at(3)}; + EXPECT_TABLE_SIZE(age_array, 22, 1, 0); + EXPECT_WILDCARD_ROW_ARRAY(23, age_array, 0, PATH()); + const auto &age_string{age_anyof_branch.children.front().children.at(4)}; + EXPECT_TABLE_SIZE(age_string, 24, 1, 0); + EXPECT_WILDCARD_ROW_PRIMITIVE( + 25, age_string, 0, PATH(), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String); + const auto &age_number{age_anyof_branch.children.front().children.at(5)}; + EXPECT_TABLE_SIZE(age_number, 26, 1, 0); + EXPECT_WILDCARD_ROW_PRIMITIVE( + 27, age_number, 0, PATH(), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Number); + + const auto &typed_branch{all_of.children.at(2)}; + EXPECT_TABLE_SIZE(typed_branch, 28, 1, 0); + EXPECT_WILDCARD_ROW_OBJECT(29, typed_branch, 0, PATH()); +} + +TEST(Documentation_2020_12, dependent_schemas_as_allof) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "dependentSchemas": { + "name": { + "properties": { "age": { "type": "integer" } } + } + } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 1); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + + EXPECT_SECTION(documentation, 0, "All of", 2); + const auto &all_of{documentation.children.front()}; + + const auto &applicator_branch{all_of.children.at(0)}; + EXPECT_TABLE_SIZE(applicator_branch, 2, 0, 1); + EXPECT_EQ(applicator_branch.children.front().label, "Any of"); + EXPECT_EQ(applicator_branch.children.front().children.size(), 2); + + const auto ¬_sub{applicator_branch.children.front().children.at(0)}; + EXPECT_TABLE_SIZE(not_sub, 3, 1, 0); + EXPECT_WILDCARD_ROW_OBJECT_WITH_CONSTRAINTS(4, not_sub, 0, PATH(), + "must NOT match >= 1 properties"); + + const auto &allof_sub{applicator_branch.children.front().children.at(1)}; + EXPECT_TABLE_SIZE(allof_sub, 5, 0, 1); + EXPECT_EQ(allof_sub.children.front().label, "All of"); + EXPECT_EQ(allof_sub.children.front().children.size(), 2); + + const auto &name_req_table{allof_sub.children.front().children.at(0)}; + EXPECT_TABLE_SIZE(name_req_table, 6, 1, 0); + EXPECT_ROW_ANY(7, name_req_table, 0, PATH(LITERAL("name")), true); + + const auto &type_expansion{allof_sub.children.front().children.at(1)}; + EXPECT_TABLE_SIZE(type_expansion, 8, 0, 1); + EXPECT_EQ(type_expansion.children.front().label, "Any of"); + EXPECT_EQ(type_expansion.children.front().children.size(), 6); + + EXPECT_TABLE_SIZE(type_expansion.children.front().children.at(0), 9, 1, 0); + EXPECT_WILDCARD_ROW_ENUM(10, type_expansion.children.front().children.at(0), + 0, PATH(), 1, 0); + EXPECT_TABLE_SIZE(type_expansion.children.front().children.at(1), 11, 1, 0); + EXPECT_WILDCARD_ROW_ENUM(12, type_expansion.children.front().children.at(1), + 0, PATH(), 2, 0); + EXPECT_TABLE_SIZE(type_expansion.children.front().children.at(2), 13, 1, 0); + EXPECT_ROW_PRIMITIVE( + 14, type_expansion.children.front().children.at(2), 0, + PATH(LITERAL("age")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Integer, + false); + EXPECT_TABLE_SIZE(type_expansion.children.front().children.at(3), 15, 1, 0); + EXPECT_WILDCARD_ROW_ARRAY(16, type_expansion.children.front().children.at(3), + 0, PATH()); + EXPECT_TABLE_SIZE(type_expansion.children.front().children.at(4), 17, 1, 0); + EXPECT_WILDCARD_ROW_PRIMITIVE( + 18, type_expansion.children.front().children.at(4), 0, PATH(), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String); + EXPECT_TABLE_SIZE(type_expansion.children.front().children.at(5), 19, 1, 0); + EXPECT_WILDCARD_ROW_PRIMITIVE( + 20, type_expansion.children.front().children.at(5), 0, PATH(), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Number); + + const auto &typed_branch{all_of.children.at(1)}; + EXPECT_TABLE_SIZE(typed_branch, 21, 1, 0); + EXPECT_ROW_PRIMITIVE( + 22, typed_branch, 0, PATH(LITERAL("name")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + false); +} + +TEST(Documentation_2020_12, dependent_required_as_allof) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { "type": "string" }, + "age": { "type": "integer" } + }, + "dependentRequired": { + "name": [ "age" ] + } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 1); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + + EXPECT_SECTION(documentation, 0, "All of", 2); + const auto &all_of{documentation.children.front()}; + + const auto &applicator_branch{all_of.children.at(0)}; + EXPECT_TABLE_SIZE(applicator_branch, 2, 0, 1); + EXPECT_EQ(applicator_branch.children.front().label, "Any of"); + EXPECT_EQ(applicator_branch.children.front().children.size(), 2); + + const auto ¬_sub{applicator_branch.children.front().children.at(0)}; + EXPECT_TABLE_SIZE(not_sub, 3, 1, 0); + EXPECT_WILDCARD_ROW_OBJECT_WITH_CONSTRAINTS(4, not_sub, 0, PATH(), + "must NOT match >= 1 properties"); + + const auto &dep_sub{applicator_branch.children.front().children.at(1)}; + EXPECT_TABLE_SIZE(dep_sub, 5, 2, 0); + EXPECT_ROW_ANY(6, dep_sub, 0, PATH(LITERAL("name")), true); + EXPECT_ROW_ANY(7, dep_sub, 1, PATH(LITERAL("age")), true); + + const auto &typed_branch{all_of.children.at(1)}; + EXPECT_TABLE_SIZE(typed_branch, 8, 2, 0); + EXPECT_ROW_PRIMITIVE( + 9, typed_branch, 0, PATH(LITERAL("name")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + false); + EXPECT_ROW_PRIMITIVE( + 10, typed_branch, 1, PATH(LITERAL("age")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Integer, + false); +} + +TEST(Documentation_2020_12, object_additional_properties_false) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "additionalProperties": false + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 2, 0); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + EXPECT_ROW_PRIMITIVE( + 2, documentation, 1, PATH(LITERAL("name")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + false); +} + +TEST(Documentation_2020_12, object_additional_properties_typed) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "additionalProperties": { "type": "integer" } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 3, 0); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + EXPECT_ROW_PRIMITIVE( + 2, documentation, 1, PATH(LITERAL("name")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + false); + EXPECT_WILDCARD_ROW_PRIMITIVE( + 3, documentation, 2, PATH(WILDCARD), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Integer); +} + +TEST(Documentation_2020_12, array_items_string) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "items": { "type": "string" } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 0); + EXPECT_ROOT_ROW_ARRAY( + 1, documentation, 0, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String); +} + +TEST(Documentation_2020_12, nested_object_with_default) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "address": { + "type": "object", + "properties": { + "street": { "type": "string" }, + "city": { "type": "string", "default": "Unknown" } + }, + "required": [ "street" ] + } + } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 4, 0); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + EXPECT_ROW_OBJECT_WITH_CONSTRAINTS( + 2, documentation, 1, PATH(LITERAL("address")), false, ">= 1 properties"); + EXPECT_ROW_PRIMITIVE( + 3, documentation, 2, PATH(LITERAL("address"), LITERAL("street")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + true); + EXPECT_ROW_PRIMITIVE_WITH_DEFAULT( + 4, documentation, 3, PATH(LITERAL("address"), LITERAL("city")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + false, sourcemeta::core::JSON{"Unknown"}); +} + +TEST(Documentation_2020_12, string_min_length) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "minLength": 1 + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 0); + EXPECT_ROOT_ROW_PRIMITIVE_WITH_CONSTRAINTS( + 1, documentation, 0, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + ">= 1 chars"); +} + +TEST(Documentation_2020_12, string_max_length) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "maxLength": 100 + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 0); + EXPECT_ROOT_ROW_PRIMITIVE_WITH_CONSTRAINTS( + 1, documentation, 0, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + "<= 100 chars"); +} + +TEST(Documentation_2020_12, string_min_max_length) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "minLength": 1, + "maxLength": 100 + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 0); + EXPECT_ROOT_ROW_PRIMITIVE_WITH_CONSTRAINTS( + 1, documentation, 0, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + ">= 1 chars", "<= 100 chars"); +} + +TEST(Documentation_2020_12, string_exact_length) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "minLength": 5, + "maxLength": 5 + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 0); + EXPECT_ROOT_ROW_PRIMITIVE_WITH_CONSTRAINTS( + 1, documentation, 0, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + "exactly 5 chars"); +} + +TEST(Documentation_2020_12, number_minimum_maximum) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number", + "minimum": 0, + "maximum": 999 + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 0); + EXPECT_ROOT_ROW_PRIMITIVE_WITH_CONSTRAINTS( + 1, documentation, 0, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Number, + ">= 0", "<= 999"); +} + +TEST(Documentation_2020_12, integer_exclusive_bounds) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "integer", + "exclusiveMinimum": 0, + "exclusiveMaximum": 100 + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 0); + EXPECT_ROOT_ROW_PRIMITIVE_WITH_CONSTRAINTS( + 1, documentation, 0, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Integer, + ">= 1", "<= 99"); +} + +TEST(Documentation_2020_12, array_min_max_items) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "items": { "type": "string" }, + "minItems": 1, + "maxItems": 10 + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 0); + EXPECT_ROOT_ROW_ARRAY_WITH_CONSTRAINTS( + 1, documentation, 0, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + ">= 1 items", "<= 10 items"); +} + +TEST(Documentation_2020_12, array_unique_items) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 0); + EXPECT_ROOT_ROW_ARRAY_WITH_CONSTRAINTS( + 1, documentation, 0, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + "unique"); +} + +TEST(Documentation_2020_12, array_unique_items_false) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "items": { "type": "string" }, + "uniqueItems": false + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 0); + EXPECT_ROOT_ROW_ARRAY( + 1, documentation, 0, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String); +} + +TEST(Documentation_2020_12, object_min_max_properties) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "minProperties": 1, + "maxProperties": 5 + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 0); + EXPECT_ROOT_ROW_OBJECT_WITH_CONSTRAINTS(1, documentation, 0, + ">= 1 properties", "<= 5 properties"); +} + +TEST(Documentation_2020_12, string_pattern) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "pattern": "^[a-z]+$" + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 0); + EXPECT_ROOT_ROW_PRIMITIVE_WITH_CONSTRAINTS( + 1, documentation, 0, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + "pattern: ^[a-z]+$"); +} + +TEST(Documentation_2020_12, number_multiple_of) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number", + "multipleOf": 0.5 + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 0); + EXPECT_ROOT_ROW_PRIMITIVE_WITH_CONSTRAINTS( + 1, documentation, 0, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Number, + "multiple of 0.5"); +} + +TEST(Documentation_2020_12, enum_single_string) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "enum": [ "active" ] + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 0); + EXPECT_ROOT_ROW_ENUM(1, documentation, 0, 1, 0); + const auto &enumeration{ + std::get( + documentation.rows.front().type.expression.value)}; + EXPECT_TRUE(enumeration.values.at(0).is_string()); + EXPECT_EQ(enumeration.values.at(0).to_string(), "active"); +} + +TEST(Documentation_2020_12, enum_multiple_strings) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "enum": [ "admin", "editor", "viewer", "guest" ] + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 0); + EXPECT_ROOT_ROW_ENUM(1, documentation, 0, 4, 0); + const auto &enumeration{ + std::get( + documentation.rows.front().type.expression.value)}; + EXPECT_EQ(enumeration.values.at(0).to_string(), "admin"); + EXPECT_EQ(enumeration.values.at(1).to_string(), "editor"); + EXPECT_EQ(enumeration.values.at(2).to_string(), "viewer"); + EXPECT_EQ(enumeration.values.at(3).to_string(), "guest"); +} + +TEST(Documentation_2020_12, enum_mixed_types) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "enum": [ "yes", "no", 1, 0, true, false, null ] + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 0); + EXPECT_ROOT_ROW_ENUM(1, documentation, 0, 7, 0); + const auto &enumeration{ + std::get( + documentation.rows.front().type.expression.value)}; + EXPECT_TRUE(enumeration.values.at(0).is_string()); + EXPECT_EQ(enumeration.values.at(0).to_string(), "yes"); + EXPECT_TRUE(enumeration.values.at(1).is_string()); + EXPECT_EQ(enumeration.values.at(1).to_string(), "no"); + EXPECT_TRUE(enumeration.values.at(2).is_integer()); + EXPECT_EQ(enumeration.values.at(2).to_integer(), 1); + EXPECT_TRUE(enumeration.values.at(3).is_integer()); + EXPECT_EQ(enumeration.values.at(3).to_integer(), 0); + EXPECT_TRUE(enumeration.values.at(4).is_boolean()); + EXPECT_TRUE(enumeration.values.at(4).to_boolean()); + EXPECT_TRUE(enumeration.values.at(5).is_boolean()); + EXPECT_FALSE(enumeration.values.at(5).to_boolean()); + EXPECT_TRUE(enumeration.values.at(6).is_null()); +} + +TEST(Documentation_2020_12, enum_exactly_ten) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "enum": [ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j" ] + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 0); + EXPECT_ROOT_ROW_ENUM(1, documentation, 0, 10, 0); +} + +TEST(Documentation_2020_12, enum_overflow) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "enum": [ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l" ] + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 0); + EXPECT_ROOT_ROW_ENUM(1, documentation, 0, 10, 2); + const auto &enumeration{ + std::get( + documentation.rows.front().type.expression.value)}; + EXPECT_EQ(enumeration.values.at(0).to_string(), "a"); + EXPECT_EQ(enumeration.values.at(9).to_string(), "j"); + EXPECT_EQ(enumeration.overflow.at(0).to_string(), "k"); + EXPECT_EQ(enumeration.overflow.at(1).to_string(), "l"); +} + +TEST(Documentation_2020_12, contains_flat_inline) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "items": { "type": "string" }, + "contains": { "type": "string", "minLength": 1 } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 0); + EXPECT_ROOT_ROW_ARRAY_WITH_CONSTRAINTS( + 1, documentation, 0, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + ">= 1 matching items", "contains >= 1 chars"); +} + +TEST(Documentation_2020_12, contains_with_min_contains) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "items": { "type": "string" }, + "contains": { "type": "string", "minLength": 1 }, + "minContains": 2 + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 0); + EXPECT_ROOT_ROW_ARRAY_WITH_CONSTRAINTS( + 1, documentation, 0, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + ">= 2 matching items", "contains >= 1 chars"); +} + +TEST(Documentation_2020_12, contains_with_max_contains) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "items": { "type": "string" }, + "contains": { "type": "string", "minLength": 1 }, + "maxContains": 5 + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 0); + EXPECT_ROOT_ROW_ARRAY_WITH_CONSTRAINTS( + 1, documentation, 0, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + ">= 1 matching items", "<= 5 matching items", "contains >= 1 chars"); +} + +TEST(Documentation_2020_12, contains_with_min_and_max_contains) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "items": { "type": "string" }, + "contains": { "type": "string", "minLength": 1 }, + "minContains": 2, + "maxContains": 5 + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 0); + EXPECT_ROOT_ROW_ARRAY_WITH_CONSTRAINTS( + 1, documentation, 0, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + ">= 2 matching items", "<= 5 matching items", "contains >= 1 chars"); +} + +TEST(Documentation_2020_12, contains_min_contains_zero) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "items": { "type": "string" }, + "contains": { "type": "string", "minLength": 1 }, + "minContains": 0 + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 0); + EXPECT_ROOT_ROW_ARRAY_WITH_CONSTRAINTS( + 1, documentation, 0, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + "0 or more matching items", "contains >= 1 chars"); +} + +TEST(Documentation_2020_12, contains_branching_section) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "items": { "type": "object" }, + "contains": { + "type": "object", + "anyOf": [ + { "properties": { "role": { "const": "admin" } } }, + { "properties": { "role": { "const": "owner" } } } + ] + } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 1); + EXPECT_ROOT_ROW_ARRAY_OF_OBJECT_WITH_CONSTRAINTS(1, documentation, 0, + ">= 1 matching items"); + + EXPECT_SECTION(documentation, 0, "Contains", 1); + const auto &contains_table{documentation.children.front().children.front()}; + EXPECT_TABLE_SIZE(contains_table, 0, 1, 1); + EXPECT_WILDCARD_ROW_OBJECT(2, contains_table, 0, + PATH(SYNTHETIC("matching item"))); + EXPECT_EQ(contains_table.children.front().label, "All of"); + EXPECT_EQ(contains_table.children.front().children.size(), 2); + + const auto &contains_applicator{ + contains_table.children.front().children.at(0)}; + EXPECT_TABLE_SIZE(contains_applicator, 3, 0, 1); + EXPECT_EQ(contains_applicator.children.front().label, "Any of"); + EXPECT_EQ(contains_applicator.children.front().children.size(), 7); + + const auto &c_null{contains_applicator.children.front().children.at(0)}; + EXPECT_TABLE_SIZE(c_null, 4, 1, 0); + EXPECT_WILDCARD_ROW_ENUM(5, c_null, 0, PATH(), 1, 0); + const auto &c_bool{contains_applicator.children.front().children.at(1)}; + EXPECT_TABLE_SIZE(c_bool, 6, 1, 0); + EXPECT_WILDCARD_ROW_ENUM(7, c_bool, 0, PATH(), 2, 0); + const auto &c_admin{contains_applicator.children.front().children.at(2)}; + EXPECT_TABLE_SIZE(c_admin, 8, 1, 0); + EXPECT_ROW_ENUM_STRINGS(9, c_admin, 0, PATH(LITERAL("role")), false, "admin"); + const auto &c_array{contains_applicator.children.front().children.at(3)}; + EXPECT_TABLE_SIZE(c_array, 10, 1, 0); + EXPECT_WILDCARD_ROW_ARRAY(11, c_array, 0, PATH()); + const auto &c_string{contains_applicator.children.front().children.at(4)}; + EXPECT_TABLE_SIZE(c_string, 12, 1, 0); + EXPECT_WILDCARD_ROW_PRIMITIVE( + 13, c_string, 0, PATH(), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String); + const auto &c_number{contains_applicator.children.front().children.at(5)}; + EXPECT_TABLE_SIZE(c_number, 14, 1, 0); + EXPECT_WILDCARD_ROW_PRIMITIVE( + 15, c_number, 0, PATH(), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Number); + const auto &c_owner{contains_applicator.children.front().children.at(6)}; + EXPECT_TABLE_SIZE(c_owner, 16, 1, 0); + EXPECT_ROW_ENUM_STRINGS(17, c_owner, 0, PATH(LITERAL("role")), false, + "owner"); + + const auto &contains_typed{contains_table.children.front().children.at(1)}; + EXPECT_TABLE_SIZE(contains_typed, 18, 1, 0); + EXPECT_WILDCARD_ROW_OBJECT(19, contains_typed, 0, PATH()); +} + +TEST(Documentation_2020_12, string_all_annotations) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "title": "Username", + "description": "The unique identifier for the user", + "default": "anonymous", + "examples": [ "alice", "bob" ], + "deprecated": true, + "readOnly": true + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 0); + EXPECT_ROOT_ROW_PRIMITIVE_WITH_MODIFIERS_AND_NOTES( + 1, documentation, 0, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + sourcemeta::blaze::Documentation::Modifier::ReadOnly, + sourcemeta::blaze::Documentation::Modifier::Deprecated, "Username", + "The unique identifier for the user", sourcemeta::core::JSON{"anonymous"}, + "alice", "bob"); +} + +TEST(Documentation_2020_12, property_write_only) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "password": { + "type": "string", + "writeOnly": true + } + } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 2, 0); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + EXPECT_ROW_PRIMITIVE_WITH_MODIFIERS( + 2, documentation, 1, PATH(LITERAL("password")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + false, sourcemeta::blaze::Documentation::Modifier::WriteOnly); +} + +TEST(Documentation_2020_12, nested_property_annotations) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "title": "User", + "description": "A user record", + "properties": { + "name": { + "type": "string", + "title": "Full name", + "description": "First and last name", + "default": "Unknown", + "examples": [ "Alice Smith" ] + }, + "token": { + "type": "string", + "readOnly": true, + "deprecated": true + } + }, + "required": [ "name" ] + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 3, 0); + EXPECT_ROOT_ROW_OBJECT_WITH_TITLE_DESCRIPTION_AND_CONSTRAINTS( + 1, documentation, 0, "User", "A user record", ">= 1 properties"); + EXPECT_ROW_PRIMITIVE_WITH_NOTES( + 2, documentation, 1, PATH(LITERAL("name")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + true, "Full name", "First and last name", + sourcemeta::core::JSON{"Unknown"}, "Alice Smith"); + EXPECT_ROW_PRIMITIVE_WITH_MODIFIERS( + 3, documentation, 2, PATH(LITERAL("token")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + false, sourcemeta::blaze::Documentation::Modifier::ReadOnly, + sourcemeta::blaze::Documentation::Modifier::Deprecated); +} + +TEST(Documentation_2020_12, content_encoding_and_media_type) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "contentEncoding": "base64", + "contentMediaType": "application/json" + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 0); + EXPECT_ROOT_ROW_PRIMITIVE_WITH_ENCODING_AND_MIME( + 1, documentation, 0, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + "base64", "application/json"); +} + +TEST(Documentation_2020_12, content_schema_flat) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "contentEncoding": "base64", + "contentMediaType": "application/json", + "contentSchema": { + "type": "object", + "minProperties": 1 + } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 0); + EXPECT_ROOT_ROW_PRIMITIVE_WITH_ENCODING_MIME_AND_CONSTRAINTS( + 1, documentation, 0, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + "base64", "application/json", "decoded >= 1 properties"); +} + +TEST(Documentation_2020_12, content_schema_branching) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "contentEncoding": "base64", + "contentMediaType": "application/json", + "contentSchema": { + "type": "object", + "properties": { + "version": { "type": "integer" } + }, + "anyOf": [ + { "properties": { "format": { "const": "v1" } } }, + { "properties": { "format": { "const": "v2" } } } + ] + } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 1); + EXPECT_ROOT_ROW_PRIMITIVE_WITH_ENCODING_AND_MIME( + 1, documentation, 0, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + "base64", "application/json"); + + EXPECT_SECTION(documentation, 0, "Decoded content", 1); + const auto &decoded_table{documentation.children.front().children.front()}; + EXPECT_TABLE_SIZE(decoded_table, 0, 1, 1); + EXPECT_WILDCARD_ROW_OBJECT(2, decoded_table, 0, PATH(SYNTHETIC("decoded"))); + EXPECT_EQ(decoded_table.children.front().label, "All of"); + EXPECT_EQ(decoded_table.children.front().children.size(), 2); + + const auto &decoded_applicator{decoded_table.children.front().children.at(0)}; + EXPECT_TABLE_SIZE(decoded_applicator, 3, 0, 1); + EXPECT_EQ(decoded_applicator.children.front().label, "Any of"); + EXPECT_EQ(decoded_applicator.children.front().children.size(), 7); + + const auto &d_null{decoded_applicator.children.front().children.at(0)}; + EXPECT_TABLE_SIZE(d_null, 4, 1, 0); + EXPECT_WILDCARD_ROW_ENUM(5, d_null, 0, PATH(), 1, 0); + const auto &d_bool{decoded_applicator.children.front().children.at(1)}; + EXPECT_TABLE_SIZE(d_bool, 6, 1, 0); + EXPECT_WILDCARD_ROW_ENUM(7, d_bool, 0, PATH(), 2, 0); + const auto &d_v1{decoded_applicator.children.front().children.at(2)}; + EXPECT_TABLE_SIZE(d_v1, 8, 1, 0); + EXPECT_ROW_ENUM_STRINGS(9, d_v1, 0, PATH(LITERAL("format")), false, "v1"); + const auto &d_array{decoded_applicator.children.front().children.at(3)}; + EXPECT_TABLE_SIZE(d_array, 10, 1, 0); + EXPECT_WILDCARD_ROW_ARRAY(11, d_array, 0, PATH()); + const auto &d_string{decoded_applicator.children.front().children.at(4)}; + EXPECT_TABLE_SIZE(d_string, 12, 1, 0); + EXPECT_WILDCARD_ROW_PRIMITIVE( + 13, d_string, 0, PATH(), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String); + const auto &d_number{decoded_applicator.children.front().children.at(5)}; + EXPECT_TABLE_SIZE(d_number, 14, 1, 0); + EXPECT_WILDCARD_ROW_PRIMITIVE( + 15, d_number, 0, PATH(), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Number); + const auto &d_v2{decoded_applicator.children.front().children.at(6)}; + EXPECT_TABLE_SIZE(d_v2, 16, 1, 0); + EXPECT_ROW_ENUM_STRINGS(17, d_v2, 0, PATH(LITERAL("format")), false, "v2"); + + const auto &decoded_typed{decoded_table.children.front().children.at(1)}; + EXPECT_TABLE_SIZE(decoded_typed, 18, 1, 0); + EXPECT_ROW_PRIMITIVE( + 19, decoded_typed, 0, PATH(LITERAL("version")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Integer, + false); +} + +TEST(Documentation_2020_12, one_of_simple) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "oneOf": [ + { + "properties": { + "method": { "const": "credit_card" }, + "card_number": { "type": "string" } + }, + "required": [ "method", "card_number" ] + }, + { + "properties": { + "method": { "const": "paypal" }, + "paypal_email": { "type": "string", "format": "email" } + }, + "required": [ "method", "paypal_email" ] + } + ] + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 1); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + + EXPECT_SECTION(documentation, 0, "All of", 2); + const auto &all_of{documentation.children.front()}; + + const auto &applicator_branch{all_of.children.at(0)}; + EXPECT_TABLE_SIZE(applicator_branch, 2, 0, 1); + EXPECT_EQ(applicator_branch.children.front().label, "One of"); + EXPECT_EQ(applicator_branch.children.front().children.size(), 2); + const auto &credit_expansion{ + applicator_branch.children.front().children.at(0)}; + EXPECT_TABLE_SIZE(credit_expansion, 3, 0, 1); + EXPECT_EQ(credit_expansion.children.front().label, "Any of"); + EXPECT_EQ(credit_expansion.children.front().children.size(), 6); + const auto &paypal_expansion{ + applicator_branch.children.front().children.at(1)}; + EXPECT_TABLE_SIZE(paypal_expansion, 17, 0, 1); + EXPECT_EQ(paypal_expansion.children.front().label, "Any of"); + EXPECT_EQ(paypal_expansion.children.front().children.size(), 6); + + const auto &typed_branch{all_of.children.at(1)}; + EXPECT_TABLE_SIZE(typed_branch, 31, 1, 0); + EXPECT_WILDCARD_ROW_OBJECT(32, typed_branch, 0, PATH()); +} + +TEST(Documentation_2020_12, property_names_flat) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1, + "maxLength": 50 + } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 0); + EXPECT_ROOT_ROW_OBJECT_WITH_CONSTRAINTS( + 1, documentation, 0, "keys >= 1 chars", "keys <= 50 chars"); +} + +TEST(Documentation_2020_12, property_names_flat_pattern) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "propertyNames": { + "type": "string", + "pattern": "^[a-z_]+$" + } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 0); + EXPECT_ROOT_ROW_OBJECT_WITH_CONSTRAINTS(1, documentation, 0, + "keys pattern: ^[a-z_]+$"); +} + +TEST(Documentation_2020_12, property_names_branching) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "propertyNames": { + "type": "string", + "anyOf": [ + { "pattern": "^x-" }, + { "pattern": "^y-" } + ] + } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 1); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + + EXPECT_SECTION(documentation, 0, "Property names", 1); + const auto &names_table{documentation.children.front().children.front()}; + EXPECT_TABLE_SIZE(names_table, 0, 1, 1); + EXPECT_WILDCARD_ROW_OBJECT(2, names_table, 0, PATH(SYNTHETIC("key"))); + EXPECT_EQ(names_table.children.front().label, "All of"); + EXPECT_EQ(names_table.children.front().children.size(), 2); + + const auto &names_applicator{names_table.children.front().children.at(0)}; + EXPECT_TABLE_SIZE(names_applicator, 3, 0, 1); + EXPECT_EQ(names_applicator.children.front().label, "Any of"); + EXPECT_EQ(names_applicator.children.front().children.size(), 7); + + const auto &n_null{names_applicator.children.front().children.at(0)}; + EXPECT_TABLE_SIZE(n_null, 4, 1, 0); + EXPECT_WILDCARD_ROW_ENUM(5, n_null, 0, PATH(), 1, 0); + const auto &n_bool{names_applicator.children.front().children.at(1)}; + EXPECT_TABLE_SIZE(n_bool, 6, 1, 0); + EXPECT_WILDCARD_ROW_ENUM(7, n_bool, 0, PATH(), 2, 0); + const auto &n_object{names_applicator.children.front().children.at(2)}; + EXPECT_TABLE_SIZE(n_object, 8, 1, 0); + EXPECT_WILDCARD_ROW_OBJECT(9, n_object, 0, PATH()); + const auto &n_array{names_applicator.children.front().children.at(3)}; + EXPECT_TABLE_SIZE(n_array, 10, 1, 0); + EXPECT_WILDCARD_ROW_ARRAY(11, n_array, 0, PATH()); + const auto &n_x_pattern{names_applicator.children.front().children.at(4)}; + EXPECT_TABLE_SIZE(n_x_pattern, 12, 1, 0); + EXPECT_WILDCARD_ROW_PRIMITIVE_WITH_CONSTRAINTS( + 13, n_x_pattern, 0, PATH(), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + "pattern: ^x-"); + const auto &n_number{names_applicator.children.front().children.at(5)}; + EXPECT_TABLE_SIZE(n_number, 14, 1, 0); + EXPECT_WILDCARD_ROW_PRIMITIVE( + 15, n_number, 0, PATH(), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Number); + const auto &n_y_pattern{names_applicator.children.front().children.at(6)}; + EXPECT_TABLE_SIZE(n_y_pattern, 16, 1, 0); + EXPECT_WILDCARD_ROW_PRIMITIVE_WITH_CONSTRAINTS( + 17, n_y_pattern, 0, PATH(), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + "pattern: ^y-"); + + const auto &names_typed{names_table.children.front().children.at(1)}; + EXPECT_TABLE_SIZE(names_typed, 18, 1, 0); + EXPECT_WILDCARD_ROW_PRIMITIVE( + 19, names_typed, 0, PATH(), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String); +} + +TEST(Documentation_2020_12, pattern_properties_only) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "patternProperties": { + "^x-": { "type": "string" } + } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 2, 0); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + EXPECT_WILDCARD_ROW_PRIMITIVE( + 2, documentation, 1, PATH(PATTERN("^x-")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String); +} + +TEST(Documentation_2020_12, pattern_properties_with_properties) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "patternProperties": { + "^x-": { "type": "string" } + } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 3, 0); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + EXPECT_ROW_PRIMITIVE( + 2, documentation, 1, PATH(LITERAL("name")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + false); + EXPECT_WILDCARD_ROW_PRIMITIVE( + 3, documentation, 2, PATH(PATTERN("^x-")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String); +} + +TEST(Documentation_2020_12, pattern_properties_with_additional) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "patternProperties": { + "^x-": { "type": "string" } + }, + "additionalProperties": { "type": "number" } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 3, 0); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + EXPECT_WILDCARD_ROW_PRIMITIVE( + 2, documentation, 1, PATH(PATTERN("^x-")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String); + EXPECT_WILDCARD_ROW_PRIMITIVE( + 3, documentation, 2, PATH(WILDCARD), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Number); +} + +TEST(Documentation_2020_12, pattern_properties_with_properties_and_additional) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "patternProperties": { + "^x-": { "type": "string" }, + "^[0-9]+$": { "type": "integer" } + }, + "additionalProperties": { "type": "number" } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 5, 0); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + EXPECT_ROW_PRIMITIVE( + 2, documentation, 1, PATH(LITERAL("name")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + false); + EXPECT_WILDCARD_ROW_PRIMITIVE( + 3, documentation, 2, PATH(PATTERN("^x-")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String); + EXPECT_WILDCARD_ROW_PRIMITIVE( + 4, documentation, 3, PATH(PATTERN("^[0-9]+$")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Integer); + EXPECT_WILDCARD_ROW_PRIMITIVE( + 5, documentation, 4, PATH(WILDCARD), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Number); +} + +TEST(Documentation_2020_12, pattern_properties_additional_false) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "id": { "type": "integer" } + }, + "patternProperties": { + "^tag_": { "type": "string" } + }, + "additionalProperties": false + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 3, 0); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + EXPECT_ROW_PRIMITIVE( + 2, documentation, 1, PATH(LITERAL("id")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Integer, + false); + EXPECT_WILDCARD_ROW_PRIMITIVE( + 3, documentation, 2, PATH(PATTERN("^tag_")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String); +} + +TEST(Documentation_2020_12, property_name_with_slash) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "foo/bar": { "type": "string" } + } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 2, 0); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + EXPECT_ROW_PRIMITIVE( + 2, documentation, 1, PATH(LITERAL("foo/bar")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + false); +} + +TEST(Documentation_2020_12, property_name_with_tilde) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "foo~bar": { "type": "string" } + } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 2, 0); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + EXPECT_ROW_PRIMITIVE( + 2, documentation, 1, PATH(LITERAL("foo~bar")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + false); +} + +TEST(Documentation_2020_12, property_name_with_slash_and_tilde) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "a/b~c": { "type": "string" } + } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 2, 0); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + EXPECT_ROW_PRIMITIVE( + 2, documentation, 1, PATH(LITERAL("a/b~c")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + false); +} + +TEST(Documentation_2020_12, tuple_prefix_items) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { "type": "string" }, + { "type": "integer" } + ] + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 3, 0); + EXPECT_ROOT_ROW_TUPLE( + 1, documentation, 0, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Integer); + EXPECT_ROW_PRIMITIVE( + 2, documentation, 1, PATH(LITERAL("0")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + false); + EXPECT_ROW_PRIMITIVE( + 3, documentation, 2, PATH(LITERAL("1")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Integer, + false); +} + +TEST(Documentation_2020_12, tuple_prefix_items_with_tail) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { "type": "string" }, + { "type": "integer" } + ], + "items": { "type": "number" } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 3, 0); + EXPECT_ROOT_ROW_TUPLE_WITH_TAIL( + 1, documentation, 0, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Number, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Integer); + EXPECT_ROW_PRIMITIVE( + 2, documentation, 1, PATH(LITERAL("0")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + false); + EXPECT_ROW_PRIMITIVE( + 3, documentation, 2, PATH(LITERAL("1")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Integer, + false); +} + +TEST(Documentation_2020_12, tuple_prefix_items_complex) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { "type": "string" }, + { + "type": "object", + "properties": { + "name": { "type": "string" }, + "age": { "type": "integer" } + }, + "required": [ "name" ] + } + ] + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 2, 1); + EXPECT_ROOT_ROW_TUPLE_PRIMITIVE_OBJECT( + 1, documentation, 0, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String); + + EXPECT_ROW_PRIMITIVE( + 2, documentation, 1, PATH(LITERAL("0")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + false); + + EXPECT_SECTION(documentation, 0, "Array item 1", 1); + const auto &item_table{documentation.children.front().children.front()}; + EXPECT_TABLE_SIZE(item_table, 3, 3, 0); + EXPECT_ROOT_ROW_OBJECT_WITH_CONSTRAINTS(4, item_table, 0, ">= 1 properties"); + EXPECT_ROW_PRIMITIVE( + 5, item_table, 1, PATH(LITERAL("name")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + true); + EXPECT_ROW_PRIMITIVE( + 6, item_table, 2, PATH(LITERAL("age")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Integer, + false); +} + +TEST(Documentation_2020_12, tuple_prefix_items_required) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { "type": "string" }, + { "type": "integer" }, + { "type": "number" } + ], + "minItems": 2 + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 4, 0); + EXPECT_ROOT_ROW_TUPLE_WITH_CONSTRAINTS( + 1, documentation, 0, ">= 2 items", + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Integer, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Number); + EXPECT_ROW_PRIMITIVE( + 2, documentation, 1, PATH(LITERAL("0")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + true); + EXPECT_ROW_PRIMITIVE( + 3, documentation, 2, PATH(LITERAL("1")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Integer, + true); + EXPECT_ROW_PRIMITIVE( + 4, documentation, 3, PATH(LITERAL("2")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Number, + false); +} + +TEST(Documentation_2020_12, not_flat_inline) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "not": { "pattern": "^admin" } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 1); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + + EXPECT_SECTION(documentation, 0, "All of", 2); + const auto &all_of{documentation.children.front()}; + + const auto ¬_branch{all_of.children.at(0)}; + EXPECT_TABLE_SIZE(not_branch, 2, 0, 1); + EXPECT_EQ(not_branch.children.front().label, "Must NOT match"); + EXPECT_EQ(not_branch.children.front().children.size(), 1); + const auto ¬_table{not_branch.children.front().children.front()}; + EXPECT_TABLE_SIZE(not_table, 0, 1, 1); + EXPECT_WILDCARD_ROW_OBJECT(3, not_table, 0, PATH(SYNTHETIC("value"))); + EXPECT_EQ(not_table.children.front().label, "Any of"); + EXPECT_EQ(not_table.children.front().children.size(), 6); + EXPECT_TABLE_SIZE(not_table.children.front().children.at(0), 4, 1, 0); + EXPECT_WILDCARD_ROW_ENUM(5, not_table.children.front().children.at(0), 0, + PATH(), 1, 0); + EXPECT_TABLE_SIZE(not_table.children.front().children.at(1), 6, 1, 0); + EXPECT_WILDCARD_ROW_ENUM(7, not_table.children.front().children.at(1), 0, + PATH(), 2, 0); + EXPECT_TABLE_SIZE(not_table.children.front().children.at(2), 8, 1, 0); + EXPECT_WILDCARD_ROW_OBJECT(9, not_table.children.front().children.at(2), 0, + PATH()); + EXPECT_TABLE_SIZE(not_table.children.front().children.at(3), 10, 1, 0); + EXPECT_WILDCARD_ROW_ARRAY(11, not_table.children.front().children.at(3), 0, + PATH()); + EXPECT_TABLE_SIZE(not_table.children.front().children.at(4), 12, 1, 0); + EXPECT_WILDCARD_ROW_PRIMITIVE_WITH_CONSTRAINTS( + 13, not_table.children.front().children.at(4), 0, PATH(), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + "pattern: ^admin"); + EXPECT_TABLE_SIZE(not_table.children.front().children.at(5), 14, 1, 0); + EXPECT_WILDCARD_ROW_PRIMITIVE( + 15, not_table.children.front().children.at(5), 0, PATH(), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Number); + + const auto &typed_branch{all_of.children.at(1)}; + EXPECT_TABLE_SIZE(typed_branch, 16, 1, 0); + EXPECT_WILDCARD_ROW_PRIMITIVE( + 17, typed_branch, 0, PATH(), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String); +} + +TEST(Documentation_2020_12, not_branching_section) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "not": { + "anyOf": [ + { "pattern": "^admin" }, + { "pattern": "^root" } + ] + } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 1); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + + EXPECT_SECTION(documentation, 0, "All of", 2); + const auto &all_of{documentation.children.front()}; + + const auto ¬_branch{all_of.children.at(0)}; + EXPECT_TABLE_SIZE(not_branch, 2, 0, 1); + EXPECT_EQ(not_branch.children.front().label, "Must NOT match"); + EXPECT_EQ(not_branch.children.front().children.size(), 1); + const auto ¬_table{not_branch.children.front().children.front()}; + EXPECT_TABLE_SIZE(not_table, 0, 1, 1); + EXPECT_WILDCARD_ROW_OBJECT(3, not_table, 0, PATH(SYNTHETIC("value"))); + EXPECT_EQ(not_table.children.front().label, "Any of"); + EXPECT_EQ(not_table.children.front().children.size(), 7); + + const auto ¬_null{not_table.children.front().children.at(0)}; + EXPECT_TABLE_SIZE(not_null, 4, 1, 0); + EXPECT_WILDCARD_ROW_ENUM(5, not_null, 0, PATH(), 1, 0); + const auto ¬_bool{not_table.children.front().children.at(1)}; + EXPECT_TABLE_SIZE(not_bool, 6, 1, 0); + EXPECT_WILDCARD_ROW_ENUM(7, not_bool, 0, PATH(), 2, 0); + const auto ¬_object{not_table.children.front().children.at(2)}; + EXPECT_TABLE_SIZE(not_object, 8, 1, 0); + EXPECT_WILDCARD_ROW_OBJECT(9, not_object, 0, PATH()); + const auto ¬_array{not_table.children.front().children.at(3)}; + EXPECT_TABLE_SIZE(not_array, 10, 1, 0); + EXPECT_WILDCARD_ROW_ARRAY(11, not_array, 0, PATH()); + const auto ¬_admin_str{not_table.children.front().children.at(4)}; + EXPECT_TABLE_SIZE(not_admin_str, 12, 1, 0); + EXPECT_WILDCARD_ROW_PRIMITIVE_WITH_CONSTRAINTS( + 13, not_admin_str, 0, PATH(), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + "pattern: ^admin"); + const auto ¬_number{not_table.children.front().children.at(5)}; + EXPECT_TABLE_SIZE(not_number, 14, 1, 0); + EXPECT_WILDCARD_ROW_PRIMITIVE( + 15, not_number, 0, PATH(), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Number); + const auto ¬_root_str{not_table.children.front().children.at(6)}; + EXPECT_TABLE_SIZE(not_root_str, 16, 1, 0); + EXPECT_WILDCARD_ROW_PRIMITIVE_WITH_CONSTRAINTS( + 17, not_root_str, 0, PATH(), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + "pattern: ^root"); + + const auto &typed_branch{all_of.children.at(1)}; + EXPECT_TABLE_SIZE(typed_branch, 18, 1, 0); + EXPECT_WILDCARD_ROW_PRIMITIVE( + 19, typed_branch, 0, PATH(), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String); +} + +TEST(Documentation_2020_12, object_unevaluated_properties) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "unevaluatedProperties": { "type": "integer" } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 3, 0); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + EXPECT_ROW_PRIMITIVE( + 2, documentation, 1, PATH(LITERAL("name")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + false); + EXPECT_WILDCARD_ROW_PRIMITIVE( + 3, documentation, 2, PATH(WILDCARD), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Integer); +} + +TEST(Documentation_2020_12, tuple_unevaluated_items) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { "type": "string" }, + { "type": "integer" } + ], + "unevaluatedItems": { "type": "number" } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 3, 0); + EXPECT_ROOT_ROW_TUPLE_WITH_TAIL( + 1, documentation, 0, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Number, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Integer); + EXPECT_ROW_PRIMITIVE( + 2, documentation, 1, PATH(LITERAL("0")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + false); + EXPECT_ROW_PRIMITIVE( + 3, documentation, 2, PATH(LITERAL("1")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Integer, + false); +} + +TEST(Documentation_2020_12, anyof_with_unevaluated_properties) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "anyOf": [ + { + "type": "object", + "properties": { "a": { "type": "string" } } + }, + { + "type": "object", + "properties": { "b": { "type": "integer" } } + } + ], + "unevaluatedProperties": { "type": "string" } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 2, 1); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + + EXPECT_SECTION(documentation, 0, "Any of", 2); + const auto &any_of{documentation.children.front()}; + const auto &branch_a{any_of.children.at(0)}; + EXPECT_TABLE_SIZE(branch_a, 2, 1, 0); + EXPECT_ROW_PRIMITIVE( + 3, branch_a, 0, PATH(LITERAL("a")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + false); + const auto &branch_b{any_of.children.at(1)}; + EXPECT_TABLE_SIZE(branch_b, 4, 1, 0); + EXPECT_ROW_PRIMITIVE( + 5, branch_b, 0, PATH(LITERAL("b")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::Integer, + false); + + EXPECT_WILDCARD_ROW_PRIMITIVE( + 6, documentation, 1, PATH(WILDCARD), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String); +} + +TEST(Documentation_2020_12, property_boolean_true) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "anything": true + } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 2, 0); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + EXPECT_ROW_ANY(2, documentation, 1, PATH(LITERAL("anything")), false); +} + +TEST(Documentation_2020_12, property_boolean_false) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "impossible": false + } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 2, 0); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + EXPECT_ROW_NEVER(2, documentation, 1, PATH(LITERAL("impossible")), false); +} + +TEST(Documentation_2020_12, root_boolean_false) { + const auto documentation{sourcemeta::blaze::to_documentation( + sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "not": true + })JSON"), + sourcemeta::core::schema_walker, sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 1); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + EXPECT_EQ(documentation.children.front().label, "Must NOT match"); + EXPECT_EQ(documentation.children.front().children.size(), 1); + const auto ¬_table{documentation.children.front().children.front()}; + EXPECT_TABLE_SIZE(not_table, 0, 1, 0); + EXPECT_WILDCARD_ROW_ANY(2, not_table, 0, PATH(SYNTHETIC("value"))); +} + +TEST(Documentation_2020_12, external_ref_property) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "address": { "$ref": "https://example.com/address.json" } + } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 2, 0); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + EXPECT_ROW_EXTERNAL_REF(2, documentation, 1, PATH(LITERAL("address")), false, + "https://example.com/address.json"); +} + +TEST(Documentation_2020_12, external_ref_root) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "https://example.com/user.json" + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 1); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + EXPECT_SECTION(documentation, 0, "All of", 1); + const auto &ref_branch{documentation.children.front().children.front()}; + EXPECT_TABLE_SIZE(ref_branch, 2, 1, 0); + EXPECT_WILDCARD_ROW_EXTERNAL_REF(3, ref_branch, 0, PATH(), + "https://example.com/user.json"); +} + +TEST(Documentation_2020_12, dynamic_ref_with_anchor) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/root", + "$dynamicAnchor": "node", + "type": "object", + "properties": { + "value": { "type": "string" }, + "children": { + "type": "array", + "items": { "$dynamicRef": "#node" } + } + } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TRUE(documentation.dynamic_anchor.has_value()); + EXPECT_EQ(documentation.dynamic_anchor.value(), "node"); + + EXPECT_TABLE_SIZE(documentation, 0, 3, 0); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + EXPECT_ROW_PRIMITIVE( + 2, documentation, 1, PATH(LITERAL("value")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + false); + + EXPECT_ROW_ARRAY_OF_RECURSIVE_REF(3, documentation, 2, + PATH(LITERAL("children")), false, 1); +} + +TEST(Documentation_2020_12, dynamic_ref_standalone) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$dynamicRef": "#target" + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 1); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + EXPECT_SECTION(documentation, 0, "All of", 1); + const auto &ref_branch{documentation.children.front().children.front()}; + EXPECT_TABLE_SIZE(ref_branch, 2, 1, 0); + EXPECT_WILDCARD_ROW_DYNAMIC_REF(3, ref_branch, 0, PATH(), "target"); +} + +TEST(Documentation_2020_12, no_dynamic_anchor_by_default) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_FALSE(documentation.dynamic_anchor.has_value()); +} + +TEST(Documentation_2020_12, internal_ref_non_recursive) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "billing": { "$ref": "#/$defs/address" }, + "shipping": { "$ref": "#/$defs/address" } + }, + "$defs": { + "address": { + "type": "object", + "properties": { + "street": { "type": "string" }, + "city": { "type": "string" } + }, + "required": [ "street" ] + } + } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 7, 0); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + EXPECT_ROW_OBJECT_WITH_CONSTRAINTS( + 2, documentation, 1, PATH(LITERAL("billing")), false, ">= 1 properties"); + EXPECT_ROW_PRIMITIVE( + 3, documentation, 2, PATH(LITERAL("billing"), LITERAL("street")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + true); + EXPECT_ROW_PRIMITIVE( + 4, documentation, 3, PATH(LITERAL("billing"), LITERAL("city")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + false); + EXPECT_ROW_OBJECT_WITH_CONSTRAINTS( + 5, documentation, 4, PATH(LITERAL("shipping")), false, ">= 1 properties"); + EXPECT_ROW_PRIMITIVE( + 6, documentation, 5, PATH(LITERAL("shipping"), LITERAL("street")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + true); + EXPECT_ROW_PRIMITIVE( + 7, documentation, 6, PATH(LITERAL("shipping"), LITERAL("city")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + false); +} + +TEST(Documentation_2020_12, internal_ref_recursive_root) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "value": { "type": "string" }, + "children": { + "type": "array", + "items": { "$ref": "#" } + } + } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 3, 0); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + EXPECT_ROW_PRIMITIVE( + 2, documentation, 1, PATH(LITERAL("value")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + false); + EXPECT_ROW_ARRAY_OF_RECURSIVE_REF( + 3, documentation, 2, PATH(LITERAL("children")), false, std::size_t{1}); +} + +TEST(Documentation_2020_12, internal_ref_recursive_via_def) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "#/$defs/node", + "$defs": { + "node": { + "type": "object", + "properties": { + "value": { "type": "string" }, + "children": { + "type": "array", + "items": { "$ref": "#/$defs/node" } + } + } + } + } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 1, 1); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + + EXPECT_SECTION(documentation, 0, "All of", 1); + const auto &node_table{documentation.children.front().children.front()}; + EXPECT_TABLE_SIZE(node_table, 2, 2, 0); + EXPECT_ROW_PRIMITIVE( + 3, node_table, 0, PATH(LITERAL("value")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + false); + EXPECT_ROW_ARRAY_OF_RECURSIVE_REF(4, node_table, 1, PATH(LITERAL("children")), + false, std::size_t{2}); +} + +TEST(Documentation_2020_12, internal_ref_recursive_with_id) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/tree", + "type": "object", + "properties": { + "value": { "type": "string" }, + "children": { + "type": "array", + "items": { "$ref": "#" } + } + } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 3, 0); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + EXPECT_ROW_PRIMITIVE( + 2, documentation, 1, PATH(LITERAL("value")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + false); + EXPECT_ROW_ARRAY_OF_RECURSIVE_REF( + 3, documentation, 2, PATH(LITERAL("children")), false, std::size_t{1}); +} + +TEST(Documentation_2020_12, internal_ref_shared_def_with_recursion) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "primary": { "$ref": "#/$defs/contact" }, + "emergency": { "$ref": "#/$defs/contact" } + }, + "$defs": { + "contact": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "backup": { "$ref": "#/$defs/contact" } + } + } + } + })JSON")}; + + const auto documentation{sourcemeta::blaze::to_documentation( + schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + EXPECT_TABLE_SIZE(documentation, 0, 7, 0); + EXPECT_ROOT_ROW_OBJECT(1, documentation, 0); + + EXPECT_ROW_OBJECT(2, documentation, 1, PATH(LITERAL("primary")), false); + EXPECT_ROW_PRIMITIVE( + 3, documentation, 2, PATH(LITERAL("primary"), LITERAL("name")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + false); + EXPECT_ROW_RECURSIVE_REF(4, documentation, 3, + PATH(LITERAL("primary"), LITERAL("backup")), false, + std::size_t{2}); + + EXPECT_ROW_OBJECT(5, documentation, 4, PATH(LITERAL("emergency")), false); + EXPECT_ROW_PRIMITIVE( + 6, documentation, 5, PATH(LITERAL("emergency"), LITERAL("name")), + sourcemeta::blaze::Documentation::Type::Expression::Primitive::String, + false); + EXPECT_ROW_RECURSIVE_REF(7, documentation, 6, + PATH(LITERAL("emergency"), LITERAL("backup")), false, + std::size_t{5}); +} diff --git a/test/documentation/documentation_test_utils.h b/test/documentation/documentation_test_utils.h new file mode 100644 index 000000000..fb4d9594a --- /dev/null +++ b/test/documentation/documentation_test_utils.h @@ -0,0 +1,1250 @@ +#ifndef SOURCEMETA_BLAZE_DOCUMENTATION_TEST_UTILS_H_ +#define SOURCEMETA_BLAZE_DOCUMENTATION_TEST_UTILS_H_ + +#include +#include +#include + +#define EXPECT_TABLE_SIZE(documentation, expected_identifier, expected_rows, \ + expected_sections) \ + EXPECT_EQ((documentation).identifier, (expected_identifier)); \ + EXPECT_EQ((documentation).rows.size(), (expected_rows)); \ + EXPECT_EQ((documentation).children.size(), (expected_sections)); + +#define LITERAL(name) \ + {.type = sourcemeta::blaze::Documentation::PathType::Literal, .value = (name)} +#define PATTERN(regex) \ + {.type = sourcemeta::blaze::Documentation::PathType::Pattern, \ + .value = (regex)} +#define WILDCARD \ + {.type = sourcemeta::blaze::Documentation::PathType::Wildcard, .value = "*"} +#define SYNTHETIC(name) \ + {.type = sourcemeta::blaze::Documentation::PathType::Synthetic, \ + .value = (name)} + +#define PATH(...) \ + (std::vector{__VA_ARGS__}) + +#define ROOT_PATH PATH(SYNTHETIC("root")) + +#define _EXPECT_PATH(row, expected_path) EXPECT_EQ((row).path, (expected_path)); + +#define _EXPECT_ROW_BASE(row) \ + EXPECT_TRUE((row).modifiers.empty()); \ + EXPECT_TRUE((row).children.empty()); + +#define _EXPECT_ROW_COMMON(row) \ + _EXPECT_ROW_BASE(row); \ + EXPECT_TRUE((row).constraints.empty()); + +#define _EXPECT_MODIFIERS(row, ...) \ + { \ + const std::vector \ + _expected_modifiers{__VA_ARGS__}; \ + EXPECT_EQ((row).modifiers.size(), _expected_modifiers.size()); \ + for (std::size_t _index{0}; _index < _expected_modifiers.size(); \ + ++_index) { \ + EXPECT_EQ((row).modifiers.at(_index), _expected_modifiers.at(_index)); \ + } \ + } + +#define EXPECT_ROOT_ROW_OBJECT(expected_identifier, documentation, index) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, ROOT_PATH); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE(std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Object>( \ + _row.type.expression.value)); \ + } + +#define EXPECT_ROOT_ROW_OBJECT_WITH_TITLE(expected_identifier, documentation, \ + index, expected_title) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, ROOT_PATH); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE(std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Object>( \ + _row.type.expression.value)); \ + EXPECT_TRUE(_row.notes.title.has_value()); \ + EXPECT_EQ(_row.notes.title.value(), (expected_title)); \ + EXPECT_FALSE(_row.notes.description.has_value()); \ + EXPECT_FALSE(_row.notes.default_value.has_value()); \ + EXPECT_TRUE(_row.notes.examples.empty()); \ + } + +#define EXPECT_ROOT_ROW_PRIMITIVE(expected_identifier, documentation, index, \ + expected_primitive) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, ROOT_PATH); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE( \ + std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _row.type.expression.value)); \ + EXPECT_EQ( \ + std::get< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _row.type.expression.value), \ + (expected_primitive)); \ + } + +#define EXPECT_ROOT_ROW_PRIMITIVE_WITH_MODIFIERS_AND_NOTES( \ + expected_identifier, documentation, index, expected_primitive, \ + expected_modifier_1, expected_modifier_2, expected_title, \ + expected_description, expected_default, ...) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, ROOT_PATH); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + EXPECT_TRUE(_row.children.empty()); \ + EXPECT_TRUE(_row.constraints.empty()); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE( \ + std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _row.type.expression.value)); \ + EXPECT_EQ( \ + std::get< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _row.type.expression.value), \ + (expected_primitive)); \ + EXPECT_EQ(_row.modifiers.size(), 2); \ + EXPECT_EQ(_row.modifiers.at(0), (expected_modifier_1)); \ + EXPECT_EQ(_row.modifiers.at(1), (expected_modifier_2)); \ + EXPECT_TRUE(_row.notes.title.has_value()); \ + EXPECT_EQ(_row.notes.title.value(), (expected_title)); \ + EXPECT_TRUE(_row.notes.description.has_value()); \ + EXPECT_EQ(_row.notes.description.value(), (expected_description)); \ + EXPECT_TRUE(_row.notes.default_value.has_value()); \ + EXPECT_EQ(_row.notes.default_value.value(), (expected_default)); \ + const std::vector _expected_examples{__VA_ARGS__}; \ + EXPECT_EQ(_row.notes.examples.size(), _expected_examples.size()); \ + for (std::size_t _ei{0}; _ei < _expected_examples.size(); ++_ei) { \ + EXPECT_EQ(_row.notes.examples.at(_ei).to_string(), \ + _expected_examples.at(_ei)); \ + } \ + } + +#define EXPECT_ROOT_ROW_ARRAY(expected_identifier, documentation, index, \ + expected_items_primitive) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, ROOT_PATH); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE(std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Array>( \ + _row.type.expression.value)); \ + const auto &_array{ \ + std::get( \ + _row.type.expression.value)}; \ + EXPECT_EQ(_array.size(), 1); \ + EXPECT_TRUE( \ + std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _array.front().value)); \ + EXPECT_EQ( \ + std::get< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _array.front().value), \ + (expected_items_primitive)); \ + } + +#define EXPECT_ROOT_ROW_TUPLE(expected_identifier, documentation, index, ...) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, ROOT_PATH); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE(std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Tuple>( \ + _row.type.expression.value)); \ + const auto &_tuple{ \ + std::get( \ + _row.type.expression.value)}; \ + const std::vector< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive> \ + _expected_items{__VA_ARGS__}; \ + EXPECT_EQ(_tuple.items.size(), _expected_items.size()); \ + for (std::size_t _index{0}; _index < _expected_items.size(); ++_index) { \ + EXPECT_TRUE( \ + std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _tuple.items.at(_index).value)); \ + EXPECT_EQ( \ + std::get< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _tuple.items.at(_index).value), \ + _expected_items.at(_index)); \ + } \ + EXPECT_TRUE(_tuple.additional.empty()); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_ROOT_ROW_TUPLE_PRIMITIVE_OBJECT( \ + expected_identifier, documentation, index, expected_primitive) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, ROOT_PATH); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE(std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Tuple>( \ + _row.type.expression.value)); \ + const auto &_tuple{ \ + std::get( \ + _row.type.expression.value)}; \ + EXPECT_EQ(_tuple.items.size(), 2); \ + EXPECT_TRUE( \ + std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _tuple.items.at(0).value)); \ + EXPECT_EQ( \ + std::get< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _tuple.items.at(0).value), \ + (expected_primitive)); \ + EXPECT_TRUE(std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Object>( \ + _tuple.items.at(1).value)); \ + EXPECT_TRUE(_tuple.additional.empty()); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_ROOT_ROW_TUPLE_WITH_TAIL(expected_identifier, documentation, \ + index, expected_tail, ...) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, ROOT_PATH); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE(std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Tuple>( \ + _row.type.expression.value)); \ + const auto &_tuple{ \ + std::get( \ + _row.type.expression.value)}; \ + const std::vector< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive> \ + _expected_items{__VA_ARGS__}; \ + EXPECT_EQ(_tuple.items.size(), _expected_items.size()); \ + for (std::size_t _index{0}; _index < _expected_items.size(); ++_index) { \ + EXPECT_TRUE( \ + std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _tuple.items.at(_index).value)); \ + EXPECT_EQ( \ + std::get< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _tuple.items.at(_index).value), \ + _expected_items.at(_index)); \ + } \ + EXPECT_EQ(_tuple.additional.size(), 1); \ + EXPECT_TRUE( \ + std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _tuple.additional.front().value)); \ + EXPECT_EQ( \ + std::get< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _tuple.additional.front().value), \ + (expected_tail)); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_ROOT_ROW_TUPLE_WITH_CONSTRAINTS( \ + expected_identifier, documentation, index, expected_constraint, ...) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, ROOT_PATH); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_BASE(_row); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE(std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Tuple>( \ + _row.type.expression.value)); \ + const auto &_tuple{ \ + std::get( \ + _row.type.expression.value)}; \ + const std::vector< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive> \ + _expected_items{__VA_ARGS__}; \ + EXPECT_EQ(_tuple.items.size(), _expected_items.size()); \ + for (std::size_t _index{0}; _index < _expected_items.size(); ++_index) { \ + EXPECT_TRUE( \ + std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _tuple.items.at(_index).value)); \ + EXPECT_EQ( \ + std::get< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _tuple.items.at(_index).value), \ + _expected_items.at(_index)); \ + } \ + EXPECT_TRUE(_tuple.additional.empty()); \ + EXPECT_EQ(_row.constraints.size(), 1); \ + EXPECT_EQ(_row.constraints.front(), (expected_constraint)); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define _EXPECT_ROW_NOTES_NONE(row) \ + EXPECT_FALSE((row).notes.title.has_value()); \ + EXPECT_FALSE((row).notes.description.has_value()); \ + EXPECT_FALSE((row).notes.default_value.has_value()); \ + EXPECT_TRUE((row).notes.examples.empty()); + +#define EXPECT_ROOT_ROW_ARRAY_OF_OBJECT(expected_identifier, documentation, \ + index) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, ROOT_PATH); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE(std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Array>( \ + _row.type.expression.value)); \ + const auto &_array{ \ + std::get( \ + _row.type.expression.value)}; \ + EXPECT_EQ(_array.size(), 1); \ + EXPECT_TRUE(std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Object>( \ + _array.front().value)); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_ROOT_ROW_ARRAY_OF_OBJECT_WITH_CONSTRAINTS( \ + expected_identifier, documentation, index, ...) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, ROOT_PATH); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_BASE(_row); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE(std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Array>( \ + _row.type.expression.value)); \ + const auto &_array{ \ + std::get( \ + _row.type.expression.value)}; \ + EXPECT_EQ(_array.size(), 1); \ + EXPECT_TRUE(std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Object>( \ + _array.front().value)); \ + _EXPECT_CONSTRAINTS(_row, __VA_ARGS__); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_ROW_OBJECT(expected_identifier, documentation, index, \ + expected_path, expected_required) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, expected_path); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_TRUE(_row.required.has_value()); \ + EXPECT_EQ(_row.required.value(), (expected_required)); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE(std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Object>( \ + _row.type.expression.value)); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_ROW_OBJECT_WITH_CONSTRAINTS(expected_identifier, documentation, \ + index, expected_path, \ + expected_required, ...) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, expected_path); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_BASE(_row); \ + EXPECT_TRUE(_row.required.has_value()); \ + EXPECT_EQ(_row.required.value(), (expected_required)); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE(std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Object>( \ + _row.type.expression.value)); \ + _EXPECT_CONSTRAINTS(_row, __VA_ARGS__); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_ROW_PRIMITIVE(expected_identifier, documentation, index, \ + expected_path, expected_primitive, \ + expected_required) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, expected_path); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_TRUE(_row.required.has_value()); \ + EXPECT_EQ(_row.required.value(), (expected_required)); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE( \ + std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _row.type.expression.value)); \ + EXPECT_EQ( \ + std::get< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _row.type.expression.value), \ + (expected_primitive)); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_ROW_PRIMITIVE_WITH_BADGE( \ + expected_identifier, documentation, index, expected_path, \ + expected_primitive, expected_required, expected_badge_kind, \ + expected_badge_value) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, expected_path); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_TRUE(_row.required.has_value()); \ + EXPECT_EQ(_row.required.value(), (expected_required)); \ + EXPECT_TRUE( \ + std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _row.type.expression.value)); \ + EXPECT_EQ( \ + std::get< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _row.type.expression.value), \ + (expected_primitive)); \ + EXPECT_EQ(_row.type.badges.size(), 1); \ + EXPECT_EQ(_row.type.badges.front().kind, (expected_badge_kind)); \ + EXPECT_EQ(_row.type.badges.front().value, (expected_badge_value)); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_ROW_PRIMITIVE_WITH_BADGE_AND_DESCRIPTION( \ + expected_identifier, documentation, index, expected_path, \ + expected_primitive, expected_required, expected_badge_kind, \ + expected_badge_value, expected_description) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, expected_path); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_TRUE(_row.required.has_value()); \ + EXPECT_EQ(_row.required.value(), (expected_required)); \ + EXPECT_TRUE( \ + std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _row.type.expression.value)); \ + EXPECT_EQ( \ + std::get< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _row.type.expression.value), \ + (expected_primitive)); \ + EXPECT_EQ(_row.type.badges.size(), 1); \ + EXPECT_EQ(_row.type.badges.front().kind, (expected_badge_kind)); \ + EXPECT_EQ(_row.type.badges.front().value, (expected_badge_value)); \ + EXPECT_FALSE(_row.notes.title.has_value()); \ + EXPECT_TRUE(_row.notes.description.has_value()); \ + EXPECT_EQ(_row.notes.description.value(), (expected_description)); \ + EXPECT_FALSE(_row.notes.default_value.has_value()); \ + EXPECT_TRUE(_row.notes.examples.empty()); \ + } + +#define EXPECT_ROW_PRIMITIVE_WITH_DESCRIPTION( \ + expected_identifier, documentation, index, expected_path, \ + expected_primitive, expected_required, expected_description) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, expected_path); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_TRUE(_row.required.has_value()); \ + EXPECT_EQ(_row.required.value(), (expected_required)); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE( \ + std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _row.type.expression.value)); \ + EXPECT_EQ( \ + std::get< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _row.type.expression.value), \ + (expected_primitive)); \ + EXPECT_FALSE(_row.notes.title.has_value()); \ + EXPECT_TRUE(_row.notes.description.has_value()); \ + EXPECT_EQ(_row.notes.description.value(), (expected_description)); \ + EXPECT_FALSE(_row.notes.default_value.has_value()); \ + EXPECT_TRUE(_row.notes.examples.empty()); \ + } + +#define EXPECT_ROW_PRIMITIVE_WITH_DEFAULT( \ + expected_identifier, documentation, index, expected_path, \ + expected_primitive, expected_required, expected_default) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, expected_path); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_TRUE(_row.required.has_value()); \ + EXPECT_EQ(_row.required.value(), (expected_required)); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE( \ + std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _row.type.expression.value)); \ + EXPECT_EQ( \ + std::get< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _row.type.expression.value), \ + (expected_primitive)); \ + EXPECT_FALSE(_row.notes.title.has_value()); \ + EXPECT_FALSE(_row.notes.description.has_value()); \ + EXPECT_TRUE(_row.notes.default_value.has_value()); \ + EXPECT_EQ(_row.notes.default_value.value(), (expected_default)); \ + EXPECT_TRUE(_row.notes.examples.empty()); \ + } + +#define EXPECT_ROW_ENUM_STRINGS(expected_identifier, documentation, index, \ + expected_path, expected_required, ...) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, expected_path); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_TRUE(_row.required.has_value()); \ + EXPECT_EQ(_row.required.value(), (expected_required)); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE( \ + std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Enumeration>( \ + _row.type.expression.value)); \ + const auto &_enum{std::get< \ + sourcemeta::blaze::Documentation::Type::Expression::Enumeration>( \ + _row.type.expression.value)}; \ + const std::vector _expected{__VA_ARGS__}; \ + EXPECT_EQ(_enum.values.size(), _expected.size()); \ + for (std::size_t _index{0}; _index < _expected.size(); ++_index) { \ + EXPECT_TRUE(_enum.values.at(_index).is_string()); \ + EXPECT_EQ(_enum.values.at(_index).to_string(), _expected.at(_index)); \ + } \ + EXPECT_TRUE(_enum.overflow.empty()); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_ROW_ENUM_BOOLEANS(expected_identifier, documentation, index, \ + expected_path, expected_required, ...) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, expected_path); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_TRUE(_row.required.has_value()); \ + EXPECT_EQ(_row.required.value(), (expected_required)); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE( \ + std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Enumeration>( \ + _row.type.expression.value)); \ + const auto &_enum{std::get< \ + sourcemeta::blaze::Documentation::Type::Expression::Enumeration>( \ + _row.type.expression.value)}; \ + const std::vector _expected{__VA_ARGS__}; \ + EXPECT_EQ(_enum.values.size(), _expected.size()); \ + for (std::size_t _index{0}; _index < _expected.size(); ++_index) { \ + EXPECT_TRUE(_enum.values.at(_index).is_boolean()); \ + EXPECT_EQ(_enum.values.at(_index).to_boolean(), _expected.at(_index)); \ + } \ + EXPECT_TRUE(_enum.overflow.empty()); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_WILDCARD_ROW_ENUM(expected_identifier, documentation, index, \ + expected_path, expected_values, \ + expected_overflow) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, expected_path); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE( \ + std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Enumeration>( \ + _row.type.expression.value)); \ + const auto &_enum{std::get< \ + sourcemeta::blaze::Documentation::Type::Expression::Enumeration>( \ + _row.type.expression.value)}; \ + EXPECT_EQ(_enum.values.size(), (expected_values)); \ + EXPECT_EQ(_enum.overflow.size(), (expected_overflow)); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_WILDCARD_ROW_ARRAY(expected_identifier, documentation, index, \ + expected_path) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, expected_path); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE(std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Array>( \ + _row.type.expression.value)); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_WILDCARD_ROW_PRIMITIVE(expected_identifier, documentation, \ + index, expected_path, \ + expected_primitive) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, expected_path); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE( \ + std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _row.type.expression.value)); \ + EXPECT_EQ( \ + std::get< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _row.type.expression.value), \ + (expected_primitive)); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_WILDCARD_ROW_ANY(expected_identifier, documentation, index, \ + expected_path) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, expected_path); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE(std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Any>( \ + _row.type.expression.value)); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_WILDCARD_ROW_NEVER(expected_identifier, documentation, index, \ + expected_path) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, expected_path); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE(std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Never>( \ + _row.type.expression.value)); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_ROOT_ROW_ANY(expected_identifier, documentation, index) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, ROOT_PATH); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE(std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Any>( \ + _row.type.expression.value)); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_ROOT_ROW_NEVER(expected_identifier, documentation, index) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, ROOT_PATH); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE(std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Never>( \ + _row.type.expression.value)); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_ROW_ANY(expected_identifier, documentation, index, \ + expected_path, expected_required) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, expected_path); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_TRUE(_row.required.has_value()); \ + EXPECT_EQ(_row.required.value(), (expected_required)); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE(std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Any>( \ + _row.type.expression.value)); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_ROW_NEVER(expected_identifier, documentation, index, \ + expected_path, expected_required) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, expected_path); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_TRUE(_row.required.has_value()); \ + EXPECT_EQ(_row.required.value(), (expected_required)); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE(std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Never>( \ + _row.type.expression.value)); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_ROW_EXTERNAL_REF(expected_identifier, documentation, index, \ + expected_path, expected_required, \ + expected_url) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, expected_path); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_TRUE(_row.required.has_value()); \ + EXPECT_EQ(_row.required.value(), (expected_required)); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE( \ + std::holds_alternative( \ + _row.type.expression.value)); \ + EXPECT_EQ(std::get(_row.type.expression.value) \ + .url, \ + (expected_url)); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_WILDCARD_ROW_EXTERNAL_REF(expected_identifier, documentation, \ + index, expected_path, expected_url) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, expected_path); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE( \ + std::holds_alternative( \ + _row.type.expression.value)); \ + EXPECT_EQ(std::get(_row.type.expression.value) \ + .url, \ + (expected_url)); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_ROW_ARRAY_OF_RECURSIVE_REF(expected_identifier, documentation, \ + index, expected_path, \ + expected_required, expected_target) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, expected_path); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_TRUE(_row.required.has_value()); \ + EXPECT_EQ(_row.required.value(), (expected_required)); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE(std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Array>( \ + _row.type.expression.value)); \ + const auto &_array{ \ + std::get( \ + _row.type.expression.value)}; \ + EXPECT_EQ(_array.size(), 1); \ + EXPECT_TRUE( \ + std::holds_alternative( \ + _array.front().value)); \ + EXPECT_EQ(std::get(_array.front().value) \ + .identifier, \ + (expected_target)); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_ROW_ARRAY_OF_DYNAMIC_REF(expected_identifier, documentation, \ + index, expected_path, \ + expected_required, expected_anchor) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, expected_path); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_TRUE(_row.required.has_value()); \ + EXPECT_EQ(_row.required.value(), (expected_required)); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE(std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Array>( \ + _row.type.expression.value)); \ + const auto &_array{ \ + std::get( \ + _row.type.expression.value)}; \ + EXPECT_EQ(_array.size(), 1); \ + EXPECT_TRUE( \ + std::holds_alternative( \ + _array.front().value)); \ + EXPECT_EQ(std::get(_array.front().value) \ + .anchor, \ + (expected_anchor)); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_ROW_RECURSIVE_REF(expected_identifier, documentation, index, \ + expected_path, expected_required, \ + expected_target) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, expected_path); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_TRUE(_row.required.has_value()); \ + EXPECT_EQ(_row.required.value(), (expected_required)); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE( \ + std::holds_alternative( \ + _row.type.expression.value)); \ + EXPECT_EQ(std::get(_row.type.expression.value) \ + .identifier, \ + (expected_target)); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_WILDCARD_ROW_RECURSIVE_REF( \ + expected_identifier, documentation, index, expected_path, expected_target) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, expected_path); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE( \ + std::holds_alternative( \ + _row.type.expression.value)); \ + EXPECT_EQ(std::get(_row.type.expression.value) \ + .identifier, \ + (expected_target)); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_WILDCARD_ROW_DYNAMIC_REF(expected_identifier, documentation, \ + index, expected_path, expected_anchor) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, expected_path); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE( \ + std::holds_alternative( \ + _row.type.expression.value)); \ + EXPECT_EQ(std::get(_row.type.expression.value) \ + .anchor, \ + (expected_anchor)); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_WILDCARD_ROW_PRIMITIVE_WITH_CONSTRAINTS( \ + expected_identifier, documentation, index, expected_path, \ + expected_primitive, ...) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, expected_path); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_BASE(_row); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE( \ + std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _row.type.expression.value)); \ + EXPECT_EQ( \ + std::get< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _row.type.expression.value), \ + (expected_primitive)); \ + _EXPECT_CONSTRAINTS(_row, __VA_ARGS__); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_WILDCARD_ROW_OBJECT(expected_identifier, documentation, index, \ + expected_path) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, expected_path); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE(std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Object>( \ + _row.type.expression.value)); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_WILDCARD_ROW_OBJECT_WITH_CONSTRAINTS( \ + expected_identifier, documentation, index, expected_path, ...) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, expected_path); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_BASE(_row); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE(std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Object>( \ + _row.type.expression.value)); \ + _EXPECT_CONSTRAINTS(_row, __VA_ARGS__); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define _EXPECT_BADGE(row, badge_index, expected_kind, expected_value) \ + EXPECT_EQ((row).type.badges.at((badge_index)).kind, (expected_kind)); \ + EXPECT_EQ((row).type.badges.at((badge_index)).value, (expected_value)); + +#define _EXPECT_CONSTRAINTS(row, ...) \ + { \ + const std::vector _expected_constraints{__VA_ARGS__}; \ + EXPECT_EQ((row).constraints.size(), _expected_constraints.size()); \ + for (std::size_t _index{0}; _index < _expected_constraints.size(); \ + ++_index) { \ + EXPECT_EQ((row).constraints.at(_index), \ + _expected_constraints.at(_index)); \ + } \ + } + +#define EXPECT_ROOT_ROW_PRIMITIVE_WITH_CONSTRAINTS( \ + expected_identifier, documentation, index, expected_primitive, ...) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, ROOT_PATH); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_BASE(_row); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE( \ + std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _row.type.expression.value)); \ + EXPECT_EQ( \ + std::get< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _row.type.expression.value), \ + (expected_primitive)); \ + _EXPECT_CONSTRAINTS(_row, __VA_ARGS__); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_ROOT_ROW_PRIMITIVE_WITH_ENCODING_MIME_AND_CONSTRAINTS( \ + expected_identifier, documentation, index, expected_primitive, \ + expected_encoding, expected_mime, ...) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, ROOT_PATH); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_BASE(_row); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE( \ + std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _row.type.expression.value)); \ + EXPECT_EQ( \ + std::get< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _row.type.expression.value), \ + (expected_primitive)); \ + EXPECT_EQ(_row.type.badges.size(), 2); \ + _EXPECT_BADGE(_row, 0, \ + sourcemeta::blaze::Documentation::Badge::Kind::Encoding, \ + expected_encoding); \ + _EXPECT_BADGE(_row, 1, \ + sourcemeta::blaze::Documentation::Badge::Kind::Mime, \ + expected_mime); \ + _EXPECT_CONSTRAINTS(_row, __VA_ARGS__); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_ROOT_ROW_PRIMITIVE_WITH_ENCODING_AND_MIME( \ + expected_identifier, documentation, index, expected_primitive, \ + expected_encoding, expected_mime) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, ROOT_PATH); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE( \ + std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _row.type.expression.value)); \ + EXPECT_EQ( \ + std::get< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _row.type.expression.value), \ + (expected_primitive)); \ + EXPECT_EQ(_row.type.badges.size(), 2); \ + _EXPECT_BADGE(_row, 0, \ + sourcemeta::blaze::Documentation::Badge::Kind::Encoding, \ + expected_encoding); \ + _EXPECT_BADGE(_row, 1, \ + sourcemeta::blaze::Documentation::Badge::Kind::Mime, \ + expected_mime); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_ROOT_ROW_OBJECT_WITH_CONSTRAINTS(expected_identifier, \ + documentation, index, ...) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, ROOT_PATH); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_BASE(_row); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE(std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Object>( \ + _row.type.expression.value)); \ + _EXPECT_CONSTRAINTS(_row, __VA_ARGS__); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_ROOT_ROW_ARRAY_WITH_CONSTRAINTS( \ + expected_identifier, documentation, index, expected_items_primitive, ...) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, ROOT_PATH); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_BASE(_row); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE(std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Array>( \ + _row.type.expression.value)); \ + const auto &_array{ \ + std::get( \ + _row.type.expression.value)}; \ + EXPECT_EQ(_array.size(), 1); \ + EXPECT_TRUE( \ + std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _array.front().value)); \ + EXPECT_EQ( \ + std::get< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _array.front().value), \ + (expected_items_primitive)); \ + _EXPECT_CONSTRAINTS(_row, __VA_ARGS__); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_ROW_PRIMITIVE_WITH_CONSTRAINTS( \ + expected_identifier, documentation, index, expected_path, \ + expected_primitive, expected_required, ...) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, expected_path); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_BASE(_row); \ + EXPECT_TRUE(_row.required.has_value()); \ + EXPECT_EQ(_row.required.value(), (expected_required)); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE( \ + std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _row.type.expression.value)); \ + EXPECT_EQ( \ + std::get< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _row.type.expression.value), \ + (expected_primitive)); \ + _EXPECT_CONSTRAINTS(_row, __VA_ARGS__); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_ROOT_ROW_ENUM(expected_identifier, documentation, index, \ + expected_values, expected_overflow) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, ROOT_PATH); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE( \ + std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Enumeration>( \ + _row.type.expression.value)); \ + const auto &_enum{std::get< \ + sourcemeta::blaze::Documentation::Type::Expression::Enumeration>( \ + _row.type.expression.value)}; \ + EXPECT_EQ(_enum.values.size(), (expected_values)); \ + EXPECT_EQ(_enum.overflow.size(), (expected_overflow)); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_SECTION(documentation, index, expected_label, \ + expected_branches) \ + EXPECT_EQ((documentation).children.at((index)).label, (expected_label)); \ + EXPECT_FALSE((documentation).children.at((index)).position.has_value()); \ + EXPECT_EQ((documentation).children.at((index)).children.size(), \ + (expected_branches)); + +#define EXPECT_ROW_PRIMITIVE_WITH_MODIFIERS( \ + expected_identifier, documentation, index, expected_path, \ + expected_primitive, expected_required, ...) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, expected_path); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + EXPECT_TRUE(_row.children.empty()); \ + EXPECT_TRUE(_row.constraints.empty()); \ + EXPECT_TRUE(_row.required.has_value()); \ + EXPECT_EQ(_row.required.value(), (expected_required)); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE( \ + std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _row.type.expression.value)); \ + EXPECT_EQ( \ + std::get< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _row.type.expression.value), \ + (expected_primitive)); \ + _EXPECT_MODIFIERS(_row, __VA_ARGS__); \ + _EXPECT_ROW_NOTES_NONE(_row); \ + } + +#define EXPECT_NOTES_TITLE_DESCRIPTION(notes, expected_title, \ + expected_description) \ + EXPECT_TRUE((notes).title.has_value()); \ + EXPECT_EQ((notes).title.value(), (expected_title)); \ + EXPECT_TRUE((notes).description.has_value()); \ + EXPECT_EQ((notes).description.value(), (expected_description)); \ + EXPECT_FALSE((notes).default_value.has_value()); \ + EXPECT_TRUE((notes).examples.empty()); + +#define EXPECT_ROOT_ROW_OBJECT_WITH_TITLE_DESCRIPTION( \ + expected_identifier, documentation, index, expected_title, \ + expected_description) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, ROOT_PATH); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE(std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Object>( \ + _row.type.expression.value)); \ + EXPECT_NOTES_TITLE_DESCRIPTION(_row.notes, expected_title, \ + expected_description); \ + } + +#define EXPECT_ROOT_ROW_OBJECT_WITH_TITLE_DESCRIPTION_AND_CONSTRAINTS( \ + expected_identifier, documentation, index, expected_title, \ + expected_description, ...) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, ROOT_PATH); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_BASE(_row); \ + EXPECT_FALSE(_row.required.has_value()); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE(std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Object>( \ + _row.type.expression.value)); \ + _EXPECT_CONSTRAINTS(_row, __VA_ARGS__); \ + EXPECT_NOTES_TITLE_DESCRIPTION(_row.notes, expected_title, \ + expected_description); \ + } + +#define EXPECT_ROW_PRIMITIVE_WITH_NOTES( \ + expected_identifier, documentation, index, expected_path, \ + expected_primitive, expected_required, expected_title, \ + expected_description, expected_default, ...) \ + { \ + const auto &_row{(documentation).rows.at((index))}; \ + _EXPECT_PATH(_row, expected_path); \ + EXPECT_EQ(_row.identifier, (expected_identifier)); \ + _EXPECT_ROW_COMMON(_row); \ + EXPECT_TRUE(_row.required.has_value()); \ + EXPECT_EQ(_row.required.value(), (expected_required)); \ + EXPECT_TRUE(_row.type.badges.empty()); \ + EXPECT_TRUE( \ + std::holds_alternative< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _row.type.expression.value)); \ + EXPECT_EQ( \ + std::get< \ + sourcemeta::blaze::Documentation::Type::Expression::Primitive>( \ + _row.type.expression.value), \ + (expected_primitive)); \ + EXPECT_TRUE((_row).notes.title.has_value()); \ + EXPECT_EQ((_row).notes.title.value(), (expected_title)); \ + EXPECT_TRUE((_row).notes.description.has_value()); \ + EXPECT_EQ((_row).notes.description.value(), (expected_description)); \ + EXPECT_TRUE((_row).notes.default_value.has_value()); \ + EXPECT_EQ((_row).notes.default_value.value(), (expected_default)); \ + const std::vector _expected_examples{__VA_ARGS__}; \ + EXPECT_EQ((_row).notes.examples.size(), _expected_examples.size()); \ + for (std::size_t _index{0}; _index < _expected_examples.size(); \ + ++_index) { \ + EXPECT_EQ((_row).notes.examples.at(_index).to_string(), \ + _expected_examples.at(_index)); \ + } \ + } + +#define EXPECT_EMPTY_BRANCH(branch) \ + EXPECT_FALSE((branch).title.has_value()); \ + EXPECT_TRUE((branch).rows.empty()); \ + EXPECT_TRUE((branch).children.empty()); + +#endif diff --git a/test/packaging/find_package/CMakeLists.txt b/test/packaging/find_package/CMakeLists.txt index d4fc9dc23..d901b9b96 100644 --- a/test/packaging/find_package/CMakeLists.txt +++ b/test/packaging/find_package/CMakeLists.txt @@ -13,3 +13,4 @@ target_link_libraries(blaze_hello PRIVATE sourcemeta::blaze::test) target_link_libraries(blaze_hello PRIVATE sourcemeta::blaze::output) target_link_libraries(blaze_hello PRIVATE sourcemeta::blaze::configuration) target_link_libraries(blaze_hello PRIVATE sourcemeta::blaze::alterschema) +target_link_libraries(blaze_hello PRIVATE sourcemeta::blaze::documentation) diff --git a/test/packaging/find_package/hello.cc b/test/packaging/find_package/hello.cc index e113e39ab..baddc4f94 100644 --- a/test/packaging/find_package/hello.cc +++ b/test/packaging/find_package/hello.cc @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include