diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a19167f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +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.1.0] - 2026-04-09 + +### Added +- New support for Avro schemas. +- Added support for `operations` section in AsyncAPI documents, allowing actions with channels and bindings to be defined. +- Extended AsyncAPI grammar to handle bindings and payload definitions in messages. \ No newline at end of file diff --git a/README.md b/README.md index db88b68..318dcf3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# 🛠️ Sonar AsyncApi (plugin) ![Release](https://img.shields.io/badge/release-1.0.1-purple) ![Swagger](https://img.shields.io/badge/-asyncapi-%2523Clojure?style=flat&logo=swagger&logoColor=white) ![Java](https://img.shields.io/badge/java-%23ED8B00.svg?style=flat&logo=openjdk&logoColor=white) [![License: LGPL v3](https://img.shields.io/badge/license-LGPL_v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0) +# 🛠️ Sonar AsyncApi (plugin) ![Release](https://img.shields.io/badge/release-1.1.0-purple) ![Swagger](https://img.shields.io/badge/-asyncapi-%2523Clojure?style=flat&logo=swagger&logoColor=white) ![Java](https://img.shields.io/badge/java-%23ED8B00.svg?style=flat&logo=openjdk&logoColor=white) [![License: LGPL v3](https://img.shields.io/badge/license-LGPL_v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0) Sonar AsyncApi (plugin) is a code analyzer for AsyncAPI specifications. Starting the project from scratch, with support from the Apiaddicts community. diff --git a/asyncapi-checks/pom.xml b/asyncapi-checks/pom.xml index c1f5134..e39ec48 100644 --- a/asyncapi-checks/pom.xml +++ b/asyncapi-checks/pom.xml @@ -5,7 +5,7 @@ org.apiaddicts.apitools.dosonarapi dosonarapi-asyncapi - 1.0.2 + 1.1.0 ../pom.xml diff --git a/asyncapi-front-end/pom.xml b/asyncapi-front-end/pom.xml index fa091f3..abc6fa8 100644 --- a/asyncapi-front-end/pom.xml +++ b/asyncapi-front-end/pom.xml @@ -5,7 +5,7 @@ org.apiaddicts.apitools.dosonarapi dosonarapi-asyncapi - 1.0.2 + 1.1.0 ../pom.xml diff --git a/asyncapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v4/AsyncApiGrammar.java b/asyncapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v4/AsyncApiGrammar.java index 2a8186e..b9ea7e6 100644 --- a/asyncapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v4/AsyncApiGrammar.java +++ b/asyncapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/api/v4/AsyncApiGrammar.java @@ -93,7 +93,8 @@ public enum AsyncApiGrammar implements GrammarRuleKey { CORRELATION_ID, BINDING_DEFINITION, HEADERS_SCHEMA, - PAYLOAD_SCHEMA; + PAYLOAD_SCHEMA, + OPERATIONS; private static final String EXTENSION_PATTERN = "^x-.*"; @@ -102,12 +103,13 @@ public static YamlGrammarBuilder create() { b.setRootRule(ROOT); b.rule(ROOT).is(b.object( - b.mandatoryProperty("asyncapi", b.firstOf("2.0.0", "2.1.0", "2.2.0", "2.3.0", "2.4.0", "2.5.0", "2.6.0")), + b.mandatoryProperty("asyncapi", b.firstOf("2.0.0", "2.1.0", "2.2.0", "2.3.0", "2.4.0", "2.5.0", "2.6.0", "3.0.0", "3.1.0")), b.property("id", b.string()), b.property("defaultContentType", b.string()), b.mandatoryProperty("info", INFO), b.property("servers", SERVERS), - b.mandatoryProperty("channels", CHANNELS), + b.property("channels", CHANNELS), + b.property("operations", OPERATIONS), b.property("components", COMPONENTS), b.property("tags", b.array(TAG)), b.property("externalDocs", EXTERNAL_DOC), @@ -130,6 +132,7 @@ public static YamlGrammarBuilder create() { buildSecurityDefinitions(b); buildTags(b); buildCallbacks(b); + buildBindings(b); return b; } @@ -214,9 +217,12 @@ private static void buildChannels(YamlGrammarBuilder b) { b.rule(CHANNEL).is(b.object( b.property("$ref", b.string()), b.property("description", DESCRIPTION), + b.property("address", b.string()), b.property("subscribe", OPERATION), b.property("publish", OPERATION), - b.property("servers", b.string()), + b.property("messages", b.object( + b.patternProperty(".*", b.firstOf(REF, MESSAGE)))), + b.property("servers", b.firstOf(b.string(), b.array(b.firstOf(REF, b.string())))), b.property("parameters", b.object( b.patternProperty(".*", PARAMETER))), b.property("bindings", CHANNEL_BINDINGS), @@ -225,6 +231,8 @@ private static void buildChannels(YamlGrammarBuilder b) { b.rule(OPERATION).is(b.object( b.property("operationId", b.string()), + b.property("action", b.string()), + b.property("channel", b.firstOf(REF, b.anything())), b.property("summary", b.string()), b.property("description", DESCRIPTION), b.property("tags", b.array(TAG)), @@ -232,6 +240,10 @@ private static void buildChannels(YamlGrammarBuilder b) { b.property("message", b.firstOf(REF, MESSAGE)), b.property("bindings", OPERATION_BINDINGS), b.patternProperty(EXTENSION_PATTERN, b.anything()))); + + b.rule(OPERATIONS).is(b.object( + b.patternProperty(".*", OPERATION), + b.patternProperty(EXTENSION_PATTERN, b.anything()))); } // Components @@ -310,6 +322,13 @@ private static void buildOperationTraits(YamlGrammarBuilder b) { // Messages private static void buildMessages(YamlGrammarBuilder b) { + b.rule(PAYLOAD_SCHEMA).is(b.firstOf( + REF, + b.object( + b.mandatoryProperty("schemaFormat", b.string()), + b.property("schema", b.anything())), + SCHEMA)); + b.rule(MESSAGE).is(b.object( b.property("name", b.string()), b.property("title", b.string()), @@ -318,7 +337,8 @@ private static void buildMessages(YamlGrammarBuilder b) { b.property("location", b.string()), b.property("contentType", b.string()), b.property("headers", b.firstOf(REF, SCHEMA)), - b.property("payload", b.firstOf(REF, SCHEMA)), + b.property("payload", PAYLOAD_SCHEMA), + b.property("bindings", MESSAGE_BINDINGS), b.property("description", DESCRIPTION), b.property("examples", b.array( b.object( @@ -387,12 +407,14 @@ private static void buildSchema(YamlGrammarBuilder b) { // Servers private static void buildServers(YamlGrammarBuilder b) { b.rule(SERVER).is(b.object( - b.mandatoryProperty("url", b.string()), + b.property("url", b.string()), + b.property("host", b.string()), b.mandatoryProperty("protocol", b.string()), b.property("description", DESCRIPTION), b.property("variables", b.object( b.patternProperty(".*", SERVER_VARIABLE))), b.property("security", b.array(SECURITY_REQUIREMENT)), + b.property("bindings", SERVER_BINDINGS), b.patternProperty(EXTENSION_PATTERN, b.anything()))); b.rule(SERVER_VARIABLE).is(b.object( @@ -428,6 +450,17 @@ private static void buildInfo(YamlGrammarBuilder b) { b.patternProperty(EXTENSION_PATTERN, b.anything()))); } + private static void buildBindings(YamlGrammarBuilder b) { + b.rule(SERVER_BINDINGS).is(b.object(b.patternProperty(".*", b.anything()))); + b.rule(CHANNEL_BINDINGS).is(b.object(b.patternProperty(".*", b.anything()))); + b.rule(OPERATION_BINDINGS).is(b.object(b.patternProperty(".*", b.anything()))); + b.rule(MESSAGE_BINDINGS).is(b.object(b.patternProperty(".*", b.anything()))); + b.rule(SERVER_BINDING).is(b.object(b.patternProperty(".*", b.anything()))); + b.rule(CHANNEL_BINDING).is(b.object(b.patternProperty(".*", b.anything()))); + b.rule(OPERATION_BINDING).is(b.object(b.patternProperty(".*", b.anything()))); + b.rule(MESSAGE_BINDING).is(b.object(b.patternProperty(".*", b.anything()))); + } + // Callbacks private static void buildCallbacks(YamlGrammarBuilder b) { b.rule(CALLBACKS).is(b.object( diff --git a/asyncapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/asyncapi/metrics/AsyncApiComplexityVisitor.java b/asyncapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/asyncapi/metrics/AsyncApiComplexityVisitor.java index 51d33d0..58d2f3d 100644 --- a/asyncapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/asyncapi/metrics/AsyncApiComplexityVisitor.java +++ b/asyncapi-front-end/src/main/java/org/apiaddicts/apitools/dosonarapi/asyncapi/metrics/AsyncApiComplexityVisitor.java @@ -31,9 +31,9 @@ public class AsyncApiComplexityVisitor extends AsyncApiVisitor { private static final HashSet COMPLEXITY_TYPES = Sets.newHashSet( AsyncApiGrammar.CHANNEL, AsyncApiGrammar.OPERATION, AsyncApiGrammar.MESSAGE, - AsyncApiGrammar.SCHEMA, AsyncApiGrammar.PARAMETER, + AsyncApiGrammar.SCHEMA, AsyncApiGrammar.PAYLOAD_SCHEMA, AsyncApiGrammar.PARAMETER, AsyncApiGrammar.CHANNEL, AsyncApiGrammar.OPERATION, AsyncApiGrammar.MESSAGE, - AsyncApiGrammar.SCHEMA, AsyncApiGrammar.PARAMETER + AsyncApiGrammar.SCHEMA, AsyncApiGrammar.PAYLOAD_SCHEMA, AsyncApiGrammar.PARAMETER ); private int complexity; diff --git a/asyncapi-front-end/src/test/java/org/apiaddicts/apitools/dosonarapi/api/v4/MessageTest.java b/asyncapi-front-end/src/test/java/org/apiaddicts/apitools/dosonarapi/api/v4/MessageTest.java new file mode 100644 index 0000000..28de6d8 --- /dev/null +++ b/asyncapi-front-end/src/test/java/org/apiaddicts/apitools/dosonarapi/api/v4/MessageTest.java @@ -0,0 +1,45 @@ +/* + * doSonarAPI: SonarQube AsyncAPI Plugin + * Copyright (C) 2024-2024 Apiaddicts + * contacta AT apiaddicts DOT org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.apiaddicts.apitools.dosonarapi.api.v4; + +import org.apiaddicts.apitools.dosonarapi.asyncapi.BaseNodeTest; +import org.junit.Test; +import org.apiaddicts.apitools.dosonarapi.sslr.yaml.grammar.JsonNode; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MessageTest extends BaseNodeTest { + + @Test + public void can_parse_avro_inline_payload() { + JsonNode node = parseResource(AsyncApiGrammar.PAYLOAD_SCHEMA, "/models/v4/avroPayload.yaml"); + assertEquals("application/vnd.apache.avro;version=1.9.0", node, "/schemaFormat"); + assertThat(node.at("/schema").isMissing()).isFalse(); + assertThat(issues).isEmpty(); + } + + @Test + public void can_parse_avro_ref_payload() { + JsonNode node = parseResource(AsyncApiGrammar.PAYLOAD_SCHEMA, "/models/v4/externalAvroPayload.yaml"); + assertEquals("application/vnd.apache.avro+json;version=1.9.0", node, "/schemaFormat"); + assertEquals("./userSchema.json", node, "/schema/$ref"); + assertThat(issues).isEmpty(); + } +} diff --git a/asyncapi-front-end/src/test/resources/models/v4/avroPayload.yaml b/asyncapi-front-end/src/test/resources/models/v4/avroPayload.yaml new file mode 100644 index 0000000..f3500fa --- /dev/null +++ b/asyncapi-front-end/src/test/resources/models/v4/avroPayload.yaml @@ -0,0 +1,11 @@ +schemaFormat: "application/vnd.apache.avro;version=1.9.0" +schema: + type: record + name: UserSignedUp + namespace: com.company + doc: User sign-up information + fields: + - name: userId + type: int + - name: userEmail + type: string diff --git a/asyncapi-front-end/src/test/resources/models/v4/externalAvroPayload.yaml b/asyncapi-front-end/src/test/resources/models/v4/externalAvroPayload.yaml new file mode 100644 index 0000000..41468e1 --- /dev/null +++ b/asyncapi-front-end/src/test/resources/models/v4/externalAvroPayload.yaml @@ -0,0 +1,3 @@ +schemaFormat: "application/vnd.apache.avro+json;version=1.9.0" +schema: + $ref: "./userSchema.json" diff --git a/asyncapi-test-tools/pom.xml b/asyncapi-test-tools/pom.xml index 8955310..920c762 100644 --- a/asyncapi-test-tools/pom.xml +++ b/asyncapi-test-tools/pom.xml @@ -5,7 +5,7 @@ org.apiaddicts.apitools.dosonarapi dosonarapi-asyncapi - 1.0.2 + 1.1.0 ../pom.xml diff --git a/pom.xml b/pom.xml index 46726ab..801f87c 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ org.apiaddicts.apitools.dosonarapi dosonarapi-asyncapi - 1.0.2 + 1.1.0 pom SonarAsyncAPI diff --git a/sonar-asyncapi-plugin/pom.xml b/sonar-asyncapi-plugin/pom.xml index 4b075dc..4a5765e 100644 --- a/sonar-asyncapi-plugin/pom.xml +++ b/sonar-asyncapi-plugin/pom.xml @@ -5,7 +5,7 @@ org.apiaddicts.apitools.dosonarapi dosonarapi-asyncapi - 1.0.2 + 1.1.0 ../pom.xml