diff --git a/src/alterschema/alterschema.cc b/src/alterschema/alterschema.cc index f3739950d..df1acc953 100644 --- a/src/alterschema/alterschema.cc +++ b/src/alterschema/alterschema.cc @@ -390,6 +390,7 @@ inline auto compile_non_standalone_subschema( #include "linter/unsatisfiable_max_contains.h" #include "linter/unsatisfiable_min_properties.h" #include "linter/valid_default.h" +#include "linter/valid_example.h" #include "linter/valid_examples.h" #undef ONLY_CONTINUE_IF @@ -497,6 +498,7 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void { bundle.add(); bundle.add(); bundle.add(); + bundle.add(); bundle.add(); } diff --git a/src/alterschema/linter/valid_example.h b/src/alterschema/linter/valid_example.h new file mode 100644 index 000000000..a42f2b915 --- /dev/null +++ b/src/alterschema/linter/valid_example.h @@ -0,0 +1,99 @@ +class ValidExample final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + ValidExample(Compiler compiler = default_schema_compiler) + : SchemaTransformRule{"valid_example", "Only set an `example` value that " + "validates against the schema"}, + compiler_{std::move(compiler)} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &root, + const sourcemeta::core::SchemaVocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &resolver) const + -> SchemaTransformRule::Result override { + using Known = SchemaVocabularies::Known; + ONLY_CONTINUE_IF(vocabularies.contains_any({Known::OPENAPI_3_0_BASE, + Known::OPENAPI_3_1_BASE, + Known::OPENAPI_3_2_BASE}) && + schema.is_object() && schema.defines("example")); + + if (vocabularies.contains(Known::OPENAPI_3_0_BASE)) { + ONLY_CONTINUE_IF(!schema.defines("$ref")); + } + + const auto &instance{schema.at("example")}; + + if (frame.standalone()) { + const auto base{frame.uri(location.pointer)}; + assert(base.has_value()); + Template schema_template; + try { + schema_template = compile(root, walker, resolver, this->compiler_, + frame, base.value().get(), Mode::Exhaustive); + } catch (const CompilerReferenceTargetNotSchemaError &) { + throw; + } catch (const sourcemeta::core::SchemaVocabularyError &) { + throw; + } catch (...) { + return false; + } + SimpleOutput output{instance}; + Evaluator evaluator; + const auto result{ + evaluator.validate(schema_template, instance, std::ref(output))}; + if (result) { + return false; + } + + std::ostringstream message; + for (const auto &entry : output) { + message << entry.message << "\n"; + message << " at instance location \""; + sourcemeta::core::stringify(entry.instance_location, message); + message << "\"\n"; + message << " at evaluate path \""; + sourcemeta::core::stringify(entry.evaluate_path, message); + message << "\"\n"; + } + + return {{{"example"}}, std::move(message).str()}; + } + + sourcemeta::core::WeakPointer base; + const auto schema_template{compile_non_standalone_subschema( + root, frame, location, walker, resolver, this->compiler_, base)}; + ONLY_CONTINUE_IF(schema_template.has_value()); + SimpleOutput output{instance, base}; + Evaluator evaluator; + const auto result{evaluator.validate(schema_template.value(), instance, + std::ref(output))}; + if (result) { + return false; + } + + std::ostringstream message; + for (const auto &entry : output) { + message << entry.message << "\n"; + message << " at instance location \""; + sourcemeta::core::stringify(entry.instance_location, message); + message << "\"\n"; + message << " at evaluate path \""; + sourcemeta::core::stringify(entry.evaluate_path, message); + message << "\"\n"; + } + + return {{{"example"}}, std::move(message).str()}; + } + + auto transform(JSON &schema, const Result &) const -> void override { + schema.erase("example"); + } + +private: + const Compiler compiler_; +}; diff --git a/test/alterschema/alterschema_lint_openapi_3_0_test.cc b/test/alterschema/alterschema_lint_openapi_3_0_test.cc index 017b2db45..835c572f6 100644 --- a/test/alterschema/alterschema_lint_openapi_3_0_test.cc +++ b/test/alterschema/alterschema_lint_openapi_3_0_test.cc @@ -1526,3 +1526,216 @@ TEST(openapi_3_0_unsatisfiable_drop_validation_with_ref_sibling) { EXPECT_EQ(document, expected); } + +TEST(openapi_3_0_valid_example) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "title": "Test", + "description": "A test schema", + "type": "string", + "example": 1 + })JSON"); + + LINT_AND_FIX_WITH_DEFAULT_DIALECT(document, result, traces, + OPENAPI_3_0_DIALECT); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "", "valid_example", + "Only set an `example` value that validates against the " + "schema", + true); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "title": "Test", + "description": "A test schema", + "type": "string" + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(openapi_3_0_valid_example_matching_instance) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "title": "Test", + "description": "A test schema", + "type": "string", + "example": "foo" + })JSON"); + + LINT_WITHOUT_FIX_WITH_DEFAULT_DIALECT(document, result, traces, + OPENAPI_3_0_DIALECT); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(openapi_3_0_valid_example_sibling_to_ref) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "title": "Test", + "description": "A test schema", + "properties": { + "foo": { "$ref": "#/properties/bar", "example": 1 }, + "bar": { "type": "string" } + } + })JSON"); + + LINT_AND_FIX_WITH_DEFAULT_DIALECT(document, result, traces, + OPENAPI_3_0_DIALECT); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "/properties/foo", "draft_ref_siblings", + "In Draft 7 and older dialects, keywords sibling to " + "`$ref` are never evaluated", + true); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "title": "Test", + "description": "A test schema", + "properties": { + "foo": { "$ref": "#/properties/bar" }, + "bar": { "type": "string" } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(openapi_3_0_duplicate_examples_not_applied) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "title": "Test", + "description": "A test schema", + "type": "string", + "examples": [ "foo", "foo" ] + })JSON"); + + LINT_AND_FIX_WITH_DEFAULT_DIALECT(document, result, traces, + OPENAPI_3_0_DIALECT); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "", "unknown_keywords_prefix", + "Future versions of JSON Schema will refuse to evaluate " + "unknown keywords or custom keywords from optional " + "vocabularies that don't have an x- prefix", + true); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "title": "Test", + "description": "A test schema", + "type": "string", + "x-examples": [ "foo", "foo" ] + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(openapi_3_0_top_level_examples_not_applied) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "title": "Test", + "description": "A test schema", + "type": "string" + })JSON"); + + LINT_WITHOUT_FIX_WITH_DEFAULT_DIALECT(document, result, traces, + OPENAPI_3_0_DIALECT); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(openapi_3_0_valid_example_with_examples) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "title": "Test", + "description": "A test schema", + "type": "string", + "example": 1, + "examples": [ 2 ] + })JSON"); + + LINT_AND_FIX_WITH_DEFAULT_DIALECT(document, result, traces, + OPENAPI_3_0_DIALECT); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 2); + EXPECT_LINT_TRACE(traces, 0, "", "unknown_keywords_prefix", + "Future versions of JSON Schema will refuse to evaluate " + "unknown keywords or custom keywords from optional " + "vocabularies that don't have an x- prefix", + true); + EXPECT_LINT_TRACE(traces, 1, "", "valid_example", + "Only set an `example` value that validates against the " + "schema", + true); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "title": "Test", + "description": "A test schema", + "type": "string", + "x-examples": [ 2 ] + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(openapi_3_0_valid_example_string_on_object_schema) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "title": "Test", + "description": "A test schema", + "type": "object", + "properties": { + "foo": { "type": "string" } + }, + "example": "bar" + })JSON"); + + LINT_AND_FIX_WITH_DEFAULT_DIALECT(document, result, traces, + OPENAPI_3_0_DIALECT); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "", "valid_example", + "Only set an `example` value that validates against the " + "schema", + true); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "title": "Test", + "description": "A test schema", + "type": "object", + "properties": { + "foo": { "type": "string" } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(openapi_3_0_valid_example_string_on_string_schema) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "title": "Test", + "description": "A test schema", + "type": "string", + "maxLength": 3, + "example": "toolong" + })JSON"); + + LINT_AND_FIX_WITH_DEFAULT_DIALECT(document, result, traces, + OPENAPI_3_0_DIALECT); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "", "valid_example", + "Only set an `example` value that validates against the " + "schema", + true); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "title": "Test", + "description": "A test schema", + "type": "string", + "maxLength": 3 + })JSON"); + + EXPECT_EQ(document, expected); +} diff --git a/test/alterschema/alterschema_lint_openapi_3_1_test.cc b/test/alterschema/alterschema_lint_openapi_3_1_test.cc index 085a44fa9..0e07555c0 100644 --- a/test/alterschema/alterschema_lint_openapi_3_1_test.cc +++ b/test/alterschema/alterschema_lint_openapi_3_1_test.cc @@ -161,3 +161,378 @@ TEST(openapi_3_1_pattern_properties_non_ecma_regex_valid) { EXPECT_TRUE(result.first); EXPECT_EQ(traces.size(), 0); } + +TEST(openapi_3_1_valid_examples) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.1/dialect/base", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string", "examples": [ 1 ] } + } + })JSON"); + + LINT_AND_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "/properties/foo", "valid_examples", + "Only include instances in the `examples` array that " + "validate against the schema", + true); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.1/dialect/base", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string" } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(openapi_3_1_valid_examples_matching_instances) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.1/dialect/base", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string", "examples": [ "bar" ] } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(openapi_3_1_duplicate_examples) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.1/dialect/base", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string", "examples": [ "bar", "bar" ] } + } + })JSON"); + + LINT_AND_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "/properties/foo", "duplicate_examples", + "Setting duplicate values in `examples` is redundant", + true); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.1/dialect/base", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string", "examples": [ "bar" ] } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(openapi_3_1_top_level_examples) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.1/dialect/base", + "title": "Test", + "description": "A test schema", + "type": "string" + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "", "top_level_examples", + "Set a non-empty examples array at the top level of the " + "schema to illustrate the expected data", + false); +} + +TEST(openapi_3_1_valid_example) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.1/dialect/base", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string", "example": 1 } + } + })JSON"); + + LINT_AND_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "/properties/foo", "valid_example", + "Only set an `example` value that validates against the " + "schema", + true); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.1/dialect/base", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string" } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(openapi_3_1_valid_example_matching_instance) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.1/dialect/base", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string", "example": "bar" } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(openapi_3_1_valid_example_sibling_to_ref) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.1/dialect/base", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "$ref": "#/$defs/helper", "example": 1 } + }, + "$defs": { + "helper": { "type": "string" } + } + })JSON"); + + LINT_AND_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "/properties/foo", "valid_example", + "Only set an `example` value that validates against the " + "schema", + true); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.1/dialect/base", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "$ref": "#/$defs/helper" } + }, + "$defs": { + "helper": { "type": "string" } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(openapi_3_1_valid_example_with_valid_examples) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.1/dialect/base", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string", "example": "bar", "examples": [ "baz" ] } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(openapi_3_1_invalid_example_with_invalid_examples) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.1/dialect/base", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string", "example": 1, "examples": [ 2 ] } + } + })JSON"); + + LINT_AND_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 2); + EXPECT_LINT_TRACE(traces, 0, "/properties/foo", "valid_example", + "Only set an `example` value that validates against the " + "schema", + true); + EXPECT_LINT_TRACE(traces, 1, "/properties/foo", "valid_examples", + "Only include instances in the `examples` array that " + "validate against the schema", + true); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.1/dialect/base", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string" } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(openapi_3_1_valid_example_with_invalid_examples) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.1/dialect/base", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string", "example": "bar", "examples": [ 1 ] } + } + })JSON"); + + LINT_AND_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "/properties/foo", "valid_examples", + "Only include instances in the `examples` array that " + "validate against the schema", + true); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.1/dialect/base", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string", "example": "bar" } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(openapi_3_1_invalid_example_with_valid_examples) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.1/dialect/base", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string", "example": 1, "examples": [ "bar" ] } + } + })JSON"); + + LINT_AND_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "/properties/foo", "valid_example", + "Only set an `example` value that validates against the " + "schema", + true); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.1/dialect/base", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string", "examples": [ "bar" ] } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(openapi_3_1_valid_example_string_on_object_schema) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.1/dialect/base", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "object", "example": "bar" } + } + })JSON"); + + LINT_AND_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "/properties/foo", "valid_example", + "Only set an `example` value that validates against the " + "schema", + true); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.1/dialect/base", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "object" } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(openapi_3_1_valid_example_string_on_string_schema) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.1/dialect/base", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string", "maxLength": 3, "example": "toolong" } + } + })JSON"); + + LINT_AND_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "/properties/foo", "valid_example", + "Only set an `example` value that validates against the " + "schema", + true); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.1/dialect/base", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string", "maxLength": 3 } + } + })JSON"); + + EXPECT_EQ(document, expected); +} diff --git a/test/alterschema/alterschema_lint_openapi_3_2_test.cc b/test/alterschema/alterschema_lint_openapi_3_2_test.cc index f7e6bb188..57281f0e1 100644 --- a/test/alterschema/alterschema_lint_openapi_3_2_test.cc +++ b/test/alterschema/alterschema_lint_openapi_3_2_test.cc @@ -161,3 +161,377 @@ TEST(openapi_3_2_pattern_properties_non_ecma_regex_valid) { EXPECT_TRUE(result.first); EXPECT_EQ(traces.size(), 0); } + +TEST(openapi_3_2_valid_examples) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.2/dialect/2025-09-17", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string", "examples": [ 1 ] } + } + })JSON"); + + LINT_AND_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "/properties/foo", "valid_examples", + "Only include instances in the `examples` array that " + "validate against the schema", + true); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.2/dialect/2025-09-17", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string" } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(openapi_3_2_valid_examples_matching_instances) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.2/dialect/2025-09-17", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string", "examples": [ "bar" ] } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(openapi_3_2_duplicate_examples) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.2/dialect/2025-09-17", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string", "examples": [ "bar", "bar" ] } + } + })JSON"); + + LINT_AND_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "/properties/foo", "duplicate_examples", + "Setting duplicate values in `examples` is redundant", + true); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.2/dialect/2025-09-17", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string", "examples": [ "bar" ] } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(openapi_3_2_top_level_examples) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.2/dialect/2025-09-17", + "title": "Test", + "description": "A test schema", + "type": "string" + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_FALSE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "", "top_level_examples", + "Set a non-empty examples array at the top level of the " + "schema to illustrate the expected data", + false); +} + +TEST(openapi_3_2_valid_example) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.2/dialect/2025-09-17", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string", "example": 1 } + } + })JSON"); + + LINT_AND_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "/properties/foo", "valid_example", + "Only set an `example` value that validates against the " + "schema", + true); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.2/dialect/2025-09-17", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string" } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(openapi_3_2_valid_example_matching_instance) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.2/dialect/2025-09-17", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string", "example": "bar" } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(openapi_3_2_valid_example_sibling_to_ref) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.2/dialect/2025-09-17", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "$ref": "#/$defs/helper", "example": 1 } + }, + "$defs": { + "helper": { "type": "string" } + } + })JSON"); + + LINT_AND_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "/properties/foo", "valid_example", + "Only set an `example` value that validates against the " + "schema", + true); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.2/dialect/2025-09-17", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "$ref": "#/$defs/helper" } + }, + "$defs": { + "helper": { "type": "string" } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(openapi_3_2_valid_example_with_valid_examples) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.2/dialect/2025-09-17", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string", "example": "bar", "examples": [ "baz" ] } + } + })JSON"); + + LINT_WITHOUT_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 0); +} + +TEST(openapi_3_2_invalid_example_with_invalid_examples) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.2/dialect/2025-09-17", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string", "example": 1, "examples": [ 2 ] } + } + })JSON"); + + LINT_AND_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 2); + EXPECT_LINT_TRACE(traces, 0, "/properties/foo", "valid_example", + "Only set an `example` value that validates against the " + "schema", + true); + EXPECT_LINT_TRACE(traces, 1, "/properties/foo", "valid_examples", + "Only include instances in the `examples` array that " + "validate against the schema", + true); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.2/dialect/2025-09-17", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string" } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(openapi_3_2_valid_example_with_invalid_examples) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.2/dialect/2025-09-17", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string", "example": "bar", "examples": [ 1 ] } + } + })JSON"); + + LINT_AND_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "/properties/foo", "valid_examples", + "Only include instances in the `examples` array that " + "validate against the schema", + true); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.2/dialect/2025-09-17", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string", "example": "bar" } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(openapi_3_2_invalid_example_with_valid_examples) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.2/dialect/2025-09-17", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string", "example": 1, "examples": [ "bar" ] } + } + })JSON"); + + LINT_AND_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "/properties/foo", "valid_example", + "Only set an `example` value that validates against the " + "schema", + true); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.2/dialect/2025-09-17", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string", "examples": [ "bar" ] } + } + })JSON"); + + EXPECT_EQ(document, expected); +} +TEST(openapi_3_2_valid_example_string_on_object_schema) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.2/dialect/2025-09-17", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "object", "example": "bar" } + } + })JSON"); + + LINT_AND_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "/properties/foo", "valid_example", + "Only set an `example` value that validates against the " + "schema", + true); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.2/dialect/2025-09-17", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "object" } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(openapi_3_2_valid_example_string_on_string_schema) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.2/dialect/2025-09-17", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string", "maxLength": 3, "example": "toolong" } + } + })JSON"); + + LINT_AND_FIX(document, result, traces); + + EXPECT_TRUE(result.first); + EXPECT_EQ(traces.size(), 1); + EXPECT_LINT_TRACE(traces, 0, "/properties/foo", "valid_example", + "Only set an `example` value that validates against the " + "schema", + true); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.2/dialect/2025-09-17", + "title": "Test", + "description": "A test schema", + "examples": [ {} ], + "properties": { + "foo": { "type": "string", "maxLength": 3 } + } + })JSON"); + + EXPECT_EQ(document, expected); +}