diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index efe3046..57b0cab 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -23,10 +23,16 @@ jobs: cache: maven - name: Build, Test and Coverage run: mvn -B verify --file pom.xml + - name: Set up JDK 21 for Sonar + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: maven - name: SonarCloud Analysis env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: mvn sonar:sonar -Dsonar.token=$SONAR_TOKEN + run: mvn org.sonarsource.scanner.maven:sonar-maven-plugin:3.11.0.3922:sonar -Dsonar.token=$SONAR_TOKEN - name: Update dependency graph uses: advanced-security/maven-dependency-submission-action@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index d97cfab..4aa9ea5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.2.2-beta-1] - 2026-08-18 + +### Added + +- Accept OpenAPI `3.0.4`, `3.1.1` and `3.1.2` in version dispatch and the v3/v3.1/v3.2 grammars. + ## [1.2.1] - 2026-06-04 ### Added diff --git a/README.md b/README.md index 852982a..8a5cd0e 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Feel free to drop by and greet us on our GitHub discussion or Discord chat. You ## ⚙️ Features -* Full compatibility with OpenAPI v2.0, v3.0.0, v3.0.1, v3.0.2, v3.0.3, v3.1.0 and v3.2.0 +* Full compatibility with OpenAPI v2.0, v3.0.0, v3.0.1, v3.0.2, v3.0.3, v3.0.4, v3.1.0, v3.1.1, v3.1.2 and v3.2.0 ![SonarOpenApi in action](sonarqube.jpg) diff --git a/its/pom.xml b/its/pom.xml index 49f6740..37277d6 100644 --- a/its/pom.xml +++ b/its/pom.xml @@ -5,7 +5,7 @@ org.apiaddicts.apitools.dosonarapi dosonarapi - 1.2.1 + 1.2.2-beta-1 ../pom.xml 4.0.0 diff --git a/openapi-checks/pom.xml b/openapi-checks/pom.xml index 4ae3c61..ae89cbb 100644 --- a/openapi-checks/pom.xml +++ b/openapi-checks/pom.xml @@ -5,7 +5,7 @@ org.apiaddicts.apitools.dosonarapi dosonarapi - 1.2.1 + 1.2.2-beta-1 ../pom.xml diff --git a/openapi-front-end/pom.xml b/openapi-front-end/pom.xml index 9c28b76..1d6729c 100644 --- a/openapi-front-end/pom.xml +++ b/openapi-front-end/pom.xml @@ -5,7 +5,7 @@ org.apiaddicts.apitools.dosonarapi dosonarapi - 1.2.1 + 1.2.2-beta-1 ../pom.xml diff --git a/openapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v3/OpenApi3Grammar.java b/openapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v3/OpenApi3Grammar.java index d275bc8..38156ed 100644 --- a/openapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v3/OpenApi3Grammar.java +++ b/openapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v3/OpenApi3Grammar.java @@ -83,7 +83,7 @@ public static YamlGrammarBuilder create() { b.setRootRule(ROOT); b.rule(ROOT).is(b.object( - b.mandatoryProperty("openapi", b.firstOf("3.0.0", "3.0.1", "3.0.2", "3.0.3")), + b.mandatoryProperty("openapi", b.firstOf("3.0.0", "3.0.1", "3.0.2", "3.0.3", "3.0.4")), b.mandatoryProperty("info", INFO), b.property("servers", b.array(SERVER)), b.mandatoryProperty("paths", PATHS), diff --git a/openapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v31/OpenApi31Grammar.java b/openapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v31/OpenApi31Grammar.java index b359e92..74c01ef 100644 --- a/openapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v31/OpenApi31Grammar.java +++ b/openapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v31/OpenApi31Grammar.java @@ -78,6 +78,7 @@ public enum OpenApi31Grammar implements GrammarRuleKey { SECURITY_SCHEMES, LINKS_COMPONENT, CALLBACKS_COMPONENT, + PATH_ITEMS_COMPONENT, SCHEMA_PROPERTIES, DESCRIPTION; @@ -88,10 +89,11 @@ public static YamlGrammarBuilder create() { b.setRootRule(ROOT); b.rule(ROOT).is(b.object( - b.mandatoryProperty("openapi", "3.1.0"), + b.mandatoryProperty("openapi", b.firstOf("3.1.0", "3.1.1", "3.1.2")), b.mandatoryProperty("info", INFO), + b.property("jsonSchemaDialect", b.string()), b.property("servers", b.array(SERVER)), - b.mandatoryProperty("paths", PATHS), + b.property("paths", PATHS), b.property("webhooks", WEBHOOKS), b.property("components", COMPONENTS), b.property("security", b.array(SECURITY_REQUIREMENT)), @@ -151,12 +153,14 @@ private static void buildComponents(YamlGrammarBuilder b) { b.property("securitySchemes", SECURITY_SCHEMES), b.property("links", LINKS_COMPONENT), b.property("callbacks", CALLBACKS_COMPONENT), + b.property("pathItems", PATH_ITEMS_COMPONENT), b.patternProperty(EXTENSION_PATTERN, b.anything()))); OpenApiGrammar.buildBaseComponentRules(b, SCHEMAS_COMPONENT, RESPONSES_COMPONENT, PARAMETERS_COMPONENT, EXAMPLES_COMPONENT, BODIES_COMPONENT, HEADERS_COMPONENT, SECURITY_SCHEMES, LINKS_COMPONENT, CALLBACKS_COMPONENT, REF, SCHEMA, RESPONSE, PARAMETER, EXAMPLE, REQUEST_BODY, HEADER, SECURITY_SCHEME, LINK, CALLBACK); b.rule(WEBHOOKS_COMPONENT).is(b.object(b.patternProperty(".*", b.firstOf(REF, WEBHOOK)))); + b.rule(PATH_ITEMS_COMPONENT).is(b.object(b.patternProperty(".*", b.firstOf(REF, PATH)))); buildParameters(b); buildResponses(b); @@ -185,16 +189,35 @@ private static void buildSchema(YamlGrammarBuilder b) { b.property("type", b.firstOf(b.string(), b.array(b.string()))), b.property("contentMediaType", b.string()), b.property("contentEncoding", b.string()), + b.property("contentSchema", b.firstOf(REF, SCHEMA)), b.property("allOf", b.array(b.firstOf(REF, SCHEMA))), b.property("oneOf", b.array(b.firstOf(REF, SCHEMA))), b.property("anyOf", b.array(b.firstOf(REF, SCHEMA))), b.property("not", b.firstOf(REF, SCHEMA)), + b.property("if", b.firstOf(REF, SCHEMA)), + b.property("then", b.firstOf(REF, SCHEMA)), + b.property("else", b.firstOf(REF, SCHEMA)), + b.property("prefixItems", b.array(b.firstOf(REF, SCHEMA))), b.property("items", b.firstOf(REF, SCHEMA)), + b.property("contains", b.firstOf(REF, SCHEMA)), + b.property("minContains", b.integer()), + b.property("maxContains", b.integer()), + b.property("unevaluatedItems", b.firstOf(b.bool(), REF, SCHEMA)), b.property("properties", SCHEMA_PROPERTIES), + b.property("patternProperties", SCHEMA_PROPERTIES), + b.property("propertyNames", b.firstOf(REF, SCHEMA)), + b.property("dependentSchemas", b.object(b.patternProperty(".*", b.firstOf(REF, SCHEMA)))), + b.property("dependentRequired", b.object(b.patternProperty(".*", b.array(b.string())))), b.property("$schema", b.string()), + b.property("$id", b.string()), + b.property("$anchor", b.string()), + b.property("$defs", b.object(b.patternProperty(".*", b.firstOf(REF, SCHEMA)))), + b.property("$dynamicRef", b.string()), + b.property("$dynamicAnchor", b.string()), + b.property("$comment", b.string()), b.property("additionalProperties", b.firstOf(b.bool(), REF, SCHEMA)), b.property("description", DESCRIPTION), - b.property("unevaluatedProperties", b.bool()), + b.property("unevaluatedProperties", b.firstOf(b.bool(), REF, SCHEMA)), b.property("format", b.string()), b.property("default", b.anything()), b.property("nullable", b.bool()), diff --git a/openapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v32/OpenApi32Grammar.java b/openapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v32/OpenApi32Grammar.java index 7a6827c..3fb0e9e 100644 --- a/openapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v32/OpenApi32Grammar.java +++ b/openapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v32/OpenApi32Grammar.java @@ -236,6 +236,7 @@ private static void buildSchema(YamlGrammarBuilder b) { b.property("type", b.firstOf(b.string(), b.array(b.string()))), b.property("contentMediaType", b.string()), b.property("contentEncoding", b.string()), + b.property("contentSchema", b.firstOf(REF, SCHEMA)), b.property("allOf", b.array(b.firstOf(REF, SCHEMA))), b.property("oneOf", b.array(b.firstOf(REF, SCHEMA))), b.property("anyOf", b.array(b.firstOf(REF, SCHEMA))), @@ -255,6 +256,7 @@ private static void buildSchema(YamlGrammarBuilder b) { b.property("dependentSchemas", b.object(b.patternProperty(".*", b.firstOf(REF, SCHEMA)))), b.property("dependentRequired", b.object(b.patternProperty(".*", b.array(b.string())))), b.property("$schema", b.string()), + b.property("$id", b.string()), b.property("$anchor", b.string()), b.property("$defs", b.object(b.patternProperty(".*", b.firstOf(REF, SCHEMA)))), b.property("$dynamicRef", b.string()), diff --git a/openapi-front-end/src/test/java/org/apiaddicts/apitools/dosonarapi/api/v31/JsonSchemaTest.java b/openapi-front-end/src/test/java/org/apiaddicts/apitools/dosonarapi/api/v31/JsonSchemaTest.java index 61a7c6f..cb5a288 100644 --- a/openapi-front-end/src/test/java/org/apiaddicts/apitools/dosonarapi/api/v31/JsonSchemaTest.java +++ b/openapi-front-end/src/test/java/org/apiaddicts/apitools/dosonarapi/api/v31/JsonSchemaTest.java @@ -23,6 +23,8 @@ import org.junit.Test; import org.apiaddicts.apitools.dosonarapi.sslr.yaml.grammar.JsonNode; +import static org.assertj.core.api.Assertions.assertThat; + public class JsonSchemaTest extends BaseNodeTest { @Test @@ -48,4 +50,23 @@ public class JsonSchemaTest extends BaseNodeTest { JsonNode param = node.at("/value"); assertEquals("http://json-schema.org/draft-07/schema#", param, "/$schema"); } + + @Test + public void json_schema_keywords() { + JsonNode node = parseResource(OpenApi31Grammar.SCHEMA, "/models/v31/jsonschemakeywords.yaml"); + + assertThat(issues).isEmpty(); + assertEquals("https://example.com/schemas/person", node, "/$id"); + assertEquals("base64", node, "/properties/id/contentEncoding"); + assertEquals("1", node, "/properties/tags/minContains"); + } + + @Test + public void root_without_paths_with_dialect_and_path_items() { + JsonNode node = parseResource(OpenApi31Grammar.ROOT, "/models/v31/webhooksonly.yaml"); + + assertThat(issues).isEmpty(); + assertEquals("https://spec.openapis.org/oas/3.1/dialect/base", node, "/jsonSchemaDialect"); + assertMissing(node.at("/paths")); + } } diff --git a/openapi-front-end/src/test/java/org/apiaddicts/apitools/dosonarapi/openapi/parser/OpenApiParserTest.java b/openapi-front-end/src/test/java/org/apiaddicts/apitools/dosonarapi/openapi/parser/OpenApiParserTest.java index 643f3b3..b2d90a4 100644 --- a/openapi-front-end/src/test/java/org/apiaddicts/apitools/dosonarapi/openapi/parser/OpenApiParserTest.java +++ b/openapi-front-end/src/test/java/org/apiaddicts/apitools/dosonarapi/openapi/parser/OpenApiParserTest.java @@ -30,6 +30,7 @@ public class OpenApiParserTest { private static final OpenApiConfiguration CONFIG = new OpenApiConfiguration(StandardCharsets.UTF_8, false); + private static final OpenApiConfiguration STRICT = new OpenApiConfiguration(StandardCharsets.UTF_8, true); private static final String MINIMAL_V2 = "swagger: \"2.0\"\n" + @@ -45,6 +46,14 @@ public class OpenApiParserTest { " version: 1.0.0\n" + "paths: {}"; + private static String minimalOpenApi(String version) { + return "openapi: \"" + version + "\"\n" + + "info:\n" + + " title: Test\n" + + " version: 1.0.0\n" + + "paths: {}"; + } + @Test public void create_v2_parser_parses_swagger_doc() { YamlParser parser = OpenApiParser.createV2(CONFIG); @@ -63,6 +72,30 @@ public void create_v3_parser_parses_openapi_doc() { assertThat(root.at("/openapi").getTokenValue()).isEqualTo("3.0.0"); } + @Test + public void create_v3_parser_accepts_openapi_304() { + YamlParser parser = OpenApiParser.createV3(STRICT); + JsonNode root = parser.parse(minimalOpenApi("3.0.4")); + assertThat(root).isNotNull(); + assertThat(root.at("/openapi").getTokenValue()).isEqualTo("3.0.4"); + } + + @Test + public void create_v31_parser_accepts_openapi_311() { + YamlParser parser = OpenApiParser.createV31(STRICT); + JsonNode root = parser.parse(minimalOpenApi("3.1.1")); + assertThat(root).isNotNull(); + assertThat(root.at("/openapi").getTokenValue()).isEqualTo("3.1.1"); + } + + @Test + public void create_v31_parser_accepts_openapi_312() { + YamlParser parser = OpenApiParser.createV31(STRICT); + JsonNode root = parser.parse(minimalOpenApi("3.1.2")); + assertThat(root).isNotNull(); + assertThat(root.at("/openapi").getTokenValue()).isEqualTo("3.1.2"); + } + @Test public void create_v31_parser_returns_non_null() { YamlParser parser = OpenApiParser.createV31(CONFIG); diff --git a/openapi-front-end/src/test/resources/models/v31/jsonschemakeywords.yaml b/openapi-front-end/src/test/resources/models/v31/jsonschemakeywords.yaml new file mode 100644 index 0000000..fc2f7e0 --- /dev/null +++ b/openapi-front-end/src/test/resources/models/v31/jsonschemakeywords.yaml @@ -0,0 +1,46 @@ +$schema: "https://json-schema.org/draft/2020-12/schema" +$id: "https://example.com/schemas/person" +$anchor: "person" +$comment: "Exercises JSON Schema keywords supported by OpenAPI 3.1" +$defs: + name: + type: string +type: [object, "null"] +properties: + id: + type: [string, "null"] + contentEncoding: base64 + contentMediaType: application/json + contentSchema: + type: object + tags: + type: array + prefixItems: + - type: string + - type: integer + items: + type: string + contains: + type: string + minContains: 1 + maxContains: 5 + unevaluatedItems: false +patternProperties: + "^x-meta-": + type: string +propertyNames: + pattern: "^[a-z]+$" +dependentSchemas: + creditCard: + required: [billingAddress] +dependentRequired: + creditCard: [billingAddress] +if: + properties: + kind: + const: admin +then: + required: [permissions] +else: + required: [role] +unevaluatedProperties: false diff --git a/openapi-front-end/src/test/resources/models/v31/webhooksonly.yaml b/openapi-front-end/src/test/resources/models/v31/webhooksonly.yaml new file mode 100644 index 0000000..b984db4 --- /dev/null +++ b/openapi-front-end/src/test/resources/models/v31/webhooksonly.yaml @@ -0,0 +1,18 @@ +openapi: "3.1.1" +info: + title: Webhooks only + version: 1.0.0 +jsonSchemaDialect: "https://spec.openapis.org/oas/3.1/dialect/base" +webhooks: + newPet: + post: + responses: + '200': + description: ok +components: + pathItems: + petItem: + get: + responses: + '200': + description: ok diff --git a/openapi-test-tools/pom.xml b/openapi-test-tools/pom.xml index cf684f6..8ed2d72 100644 --- a/openapi-test-tools/pom.xml +++ b/openapi-test-tools/pom.xml @@ -5,7 +5,7 @@ org.apiaddicts.apitools.dosonarapi dosonarapi - 1.2.1 + 1.2.2-beta-1 ../pom.xml diff --git a/pom.xml b/pom.xml index 5719259..c47f9ce 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ org.apiaddicts.apitools.dosonarapi dosonarapi - 1.2.1 + 1.2.2-beta-1 pom SonarOpenAPI diff --git a/sonar-openapi-plugin/pom.xml b/sonar-openapi-plugin/pom.xml index 957db01..e365ba6 100644 --- a/sonar-openapi-plugin/pom.xml +++ b/sonar-openapi-plugin/pom.xml @@ -5,7 +5,7 @@ org.apiaddicts.apitools.dosonarapi dosonarapi - 1.2.1 + 1.2.2-beta-1 ../pom.xml diff --git a/sonar-openapi-plugin/src/main/java/org/apiaddicts/apitools/dosonarapi/plugin/OpenApiAnalyzer.java b/sonar-openapi-plugin/src/main/java/org/apiaddicts/apitools/dosonarapi/plugin/OpenApiAnalyzer.java index 8ed5257..5f4fe9d 100644 --- a/sonar-openapi-plugin/src/main/java/org/apiaddicts/apitools/dosonarapi/plugin/OpenApiAnalyzer.java +++ b/sonar-openapi-plugin/src/main/java/org/apiaddicts/apitools/dosonarapi/plugin/OpenApiAnalyzer.java @@ -112,7 +112,13 @@ public void scanFiles() { private static boolean isV3Version(JsonNode openapiNode) { if (openapiNode.isMissing()) return false; String v = openapiNode.getTokenValue(); - return "3.0.0".equals(v) || "3.0.1".equals(v) || "3.0.2".equals(v) || "3.0.3".equals(v); + return "3.0.0".equals(v) || "3.0.1".equals(v) || "3.0.2".equals(v) || "3.0.3".equals(v) || "3.0.4".equals(v); + } + + private static boolean isV31Version(JsonNode openapiNode) { + if (openapiNode.isMissing()) return false; + String v = openapiNode.getTokenValue(); + return "3.1.0".equals(v) || "3.1.1".equals(v) || "3.1.2".equals(v); } private YamlParser selectParser(boolean isV2, boolean isV3, boolean isV31, boolean isV32) { @@ -133,7 +139,7 @@ private void scanFile(InputFile inputFile) { boolean isV2 = !rootNode.at("/swagger").isMissing(); JsonNode openapiNode = rootNode.at("/openapi"); boolean isV3 = isV3Version(openapiNode); - boolean isV31 = !openapiNode.isMissing() && openapiNode.getTokenValue().equals("3.1.0"); + boolean isV31 = isV31Version(openapiNode); boolean isV32 = !openapiNode.isMissing() && openapiNode.getTokenValue().equals("3.2.0"); YamlParser targetParser = selectParser(isV2, isV3, isV31, isV32); if (targetParser == null) return;