diff --git a/CHANGELOG.md b/CHANGELOG.md index 2613f54..e4e8478 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,11 @@ 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). -## [2.0.2-beta-3] - 2026-07-15 +## [2.0.2-beta-3] - 2026-07-21 ### Added - **AAR050InfoTitleRequiredCheck**: The `info.title` field must exist and not be empty, so the AsyncAPI title reliably identifies the messaging contract (BUG / MAJOR). +- **AAR056AvroSchemaFormatCheck**: Wherever `schemaFormat` appears in the document (message-level in v2, `message.payload` Multi-Format Schema Object in v3, or a `components.schemas` entry) and indicates Avro, it must be exactly `application/vnd.apache.avro;version=1.9.0`, the standard version used across the Style Guide examples (BUG / MAJOR). ## [2.0.2-beta-2] - 2026-06-25 diff --git a/README.md b/README.md index 2fcfed0..b8f93c6 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,7 @@ This plugin is supported by SonarQube versions greater or equal to `6.7.4` - **AAR042MessageIdentifierCheck**: It is recommended to have a unique identifier per message. - **AAR043SecurityChannelCheck**: It is recommended to add the security scheme to be used to each channel. - **AAR050InfoTitleRequiredCheck**: The info.title field must exist and not be empty. +- **AAR056AvroSchemaFormatCheck**: When `schemaFormat` indicates Avro, it must be exactly `application/vnd.apache.avro;version=1.9.0`. ## 💛 Sponsors cloudappi diff --git a/src/main/java/apiquality/sonar/asyncapi/checks/RulesLists.java b/src/main/java/apiquality/sonar/asyncapi/checks/RulesLists.java index 968aedb..7f879cc 100644 --- a/src/main/java/apiquality/sonar/asyncapi/checks/RulesLists.java +++ b/src/main/java/apiquality/sonar/asyncapi/checks/RulesLists.java @@ -72,7 +72,8 @@ public static List> getSchemasChecks() { AAR046AvroRecordDocCheck.class, AAR047AvroFieldDocCheck.class, AAR048AvroNameNomenclatureCheck.class, - AAR049AvroDefaultNullCheck.class + AAR049AvroDefaultNullCheck.class, + AAR056AvroSchemaFormatCheck.class ); } diff --git a/src/main/java/apiquality/sonar/asyncapi/checks/schemas/AAR056AvroSchemaFormatCheck.java b/src/main/java/apiquality/sonar/asyncapi/checks/schemas/AAR056AvroSchemaFormatCheck.java new file mode 100644 index 0000000..0b8362e --- /dev/null +++ b/src/main/java/apiquality/sonar/asyncapi/checks/schemas/AAR056AvroSchemaFormatCheck.java @@ -0,0 +1,70 @@ +package apiquality.sonar.asyncapi.checks.schemas; + +import com.google.common.collect.Sets; +import com.sonar.sslr.api.AstNodeType; +import org.sonar.check.Rule; +import org.apiaddicts.apitools.dosonarapi.api.v4.AsyncApiGrammar; +import apiquality.sonar.asyncapi.checks.BaseCheck; +import apiquality.sonar.asyncapi.utils.AvroUtils; +import org.apiaddicts.apitools.dosonarapi.sslr.yaml.grammar.JsonNode; + +import java.util.Map; +import java.util.Set; + +@Rule(key = AAR056AvroSchemaFormatCheck.CHECK_KEY) +public class AAR056AvroSchemaFormatCheck extends BaseCheck { + public static final String CHECK_KEY = "AAR056"; + private static final String ERROR_KEY = "AAR056.error"; + + private static final String SCHEMA_FORMAT_KEY = "schemaFormat"; + private static final String EXPECTED_SCHEMA_FORMAT = "application/vnd.apache.avro;version=1.9.0"; + + @Override + public Set subscribedKinds() { + return Sets.newHashSet(AsyncApiGrammar.ROOT); + } + + @Override + protected void visitNode(JsonNode rootNode) { + visit(rootNode); + } + + private void visit(JsonNode node) { + if (node == null || node.isMissing() || node.isNull()) { + return; + } + if (node.isArray()) { + for (JsonNode element : node.elements()) { + visit(element); + } + return; + } + if (!node.isObject()) { + return; + } + for (Map.Entry entry : node.propertyMap().entrySet()) { + if (SCHEMA_FORMAT_KEY.equals(entry.getKey())) { + validateSchemaFormat(entry.getValue()); + } + visit(entry.getValue()); + } + } + + private void validateSchemaFormat(JsonNode schemaFormatNode) { + if (schemaFormatNode == null || schemaFormatNode.isMissing() || schemaFormatNode.isNull()) { + return; + } + if (schemaFormatNode.isObject() || schemaFormatNode.isArray()) { + return; + } + + String value = schemaFormatNode.stringValue(); + if (value == null || !value.contains(AvroUtils.AVRO_SCHEMA_FORMAT_PREFIX)) { + return; + } + + if (!EXPECTED_SCHEMA_FORMAT.equals(value)) { + addIssue(CHECK_KEY, translate(ERROR_KEY, value, EXPECTED_SCHEMA_FORMAT), schemaFormatNode.key()); + } + } +} diff --git a/src/main/resources/messages/errors.properties b/src/main/resources/messages/errors.properties index a69edc4..d55c3bb 100644 --- a/src/main/resources/messages/errors.properties +++ b/src/main/resources/messages/errors.properties @@ -39,3 +39,4 @@ AAR047.error=Avro field should include the doc attribute with a description AAR048.error=Avro name must start with a letter or underscore, and can only contain letters, numbers and underscores AAR049.error=Optional Avro fields (union with null) must declare their default value as null AAR050.error=The info object must contain a non-empty title field +AAR056.error=The ''schemaFormat'' value ''{0}'' must be exactly ''{1}'' when the payload uses Avro diff --git a/src/main/resources/messages/errors_es.properties b/src/main/resources/messages/errors_es.properties index a22f5a9..d01abd7 100644 --- a/src/main/resources/messages/errors_es.properties +++ b/src/main/resources/messages/errors_es.properties @@ -36,4 +36,5 @@ AAR046.error=El registro Avro debería incluir el campo doc con una descripción AAR047.error=El campo Avro debería incluir el atributo doc con una descripción AAR048.error=El nombre Avro debe empezar con una letra o guion bajo, y solo puede contener letras, números y guiones bajos AAR049.error=Los campos opcionales de Avro (unión con null) deben declarar su valor por defecto como null -AAR050.error=El objeto info debe contener un campo title no vacío \ No newline at end of file +AAR050.error=El objeto info debe contener un campo title no vacío +AAR056.error=El valor de ''schemaFormat'' ''{0}'' debe ser exactamente ''{1}'' cuando el payload usa Avro diff --git a/src/main/resources/org/sonar/l10n/asyncapi/rules/asyncapi/schemas/AAR056.html b/src/main/resources/org/sonar/l10n/asyncapi/rules/asyncapi/schemas/AAR056.html new file mode 100644 index 0000000..68d211b --- /dev/null +++ b/src/main/resources/org/sonar/l10n/asyncapi/rules/asyncapi/schemas/AAR056.html @@ -0,0 +1,82 @@ +

When a message payload (AsyncAPI 2) or a Multi-Format Schema Object (AsyncAPI 3, on message.payload or a components.schemas entry) declares an Avro schemaFormat — i.e. its value contains application/vnd.apache.avro — that value must be exactly application/vnd.apache.avro;version=1.9.0.

+

Version 1.9.0 is the standard Avro schema version and appears in every example throughout the Style Guide. Any other version string, or a missing version, is reported as an issue. schemaFormat values that are not Avro at all (e.g. a JSON Schema schemaFormat) are out of scope for this rule.

+

Noncompliant Code Example (AsyncAPI 2)

+
+asyncapi: '2.6.0'
+info:
+  title: Carga API
+  version: '1.0.0'
+channels:
+  carga:
+    subscribe:
+      message:
+        schemaFormat: 'application/vnd.apache.avro;version=1.8.0'
+        payload:
+          type: record
+          name: CargaValue
+          namespace: com.example.avro
+          fields:
+            - name: id
+              type: string
+
+

Compliant Solution (AsyncAPI 2)

+
+asyncapi: '2.6.0'
+info:
+  title: Carga API
+  version: '1.0.0'
+channels:
+  carga:
+    subscribe:
+      message:
+        schemaFormat: 'application/vnd.apache.avro;version=1.9.0'
+        payload:
+          type: record
+          name: CargaValue
+          namespace: com.example.avro
+          fields:
+            - name: id
+              type: string
+
+

Noncompliant Code Example (AsyncAPI 3)

+
+asyncapi: 3.0.0
+info:
+  title: Carga API
+  version: 1.0.0
+channels:
+  carga:
+    address: carga
+    messages:
+      CargaMessage:
+        payload:
+          schemaFormat: 'application/vnd.apache.avro;version=1.8.0'
+          schema:
+            type: record
+            name: CargaValue
+            namespace: com.example.avro
+            fields:
+              - name: id
+                type: string
+
+

Compliant Solution (AsyncAPI 3)

+
+asyncapi: 3.0.0
+info:
+  title: Carga API
+  version: 1.0.0
+channels:
+  carga:
+    address: carga
+    messages:
+      CargaMessage:
+        payload:
+          schemaFormat: 'application/vnd.apache.avro;version=1.9.0'
+          schema:
+            type: record
+            name: CargaValue
+            namespace: com.example.avro
+            fields:
+              - name: id
+                type: string
+
diff --git a/src/main/resources/org/sonar/l10n/asyncapi/rules/asyncapi/schemas/AAR056.json b/src/main/resources/org/sonar/l10n/asyncapi/rules/asyncapi/schemas/AAR056.json new file mode 100644 index 0000000..7d5c09a --- /dev/null +++ b/src/main/resources/org/sonar/l10n/asyncapi/rules/asyncapi/schemas/AAR056.json @@ -0,0 +1,13 @@ +{ + "title": "AAR056 - AvroSchemaFormat - The schemaFormat must be application/vnd.apache.avro;version=1.9.0", + "type": "BUG", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5mn" + }, + "tags": [ + "schemas" + ], + "defaultSeverity": "MAJOR" +} diff --git a/src/main/resources/org/sonar/l10n/es/asyncapi/rules/asyncapi/schemas/AAR056.html b/src/main/resources/org/sonar/l10n/es/asyncapi/rules/asyncapi/schemas/AAR056.html new file mode 100644 index 0000000..033e8ba --- /dev/null +++ b/src/main/resources/org/sonar/l10n/es/asyncapi/rules/asyncapi/schemas/AAR056.html @@ -0,0 +1,82 @@ +

Cuando el payload de un mensaje (AsyncAPI 2) o un Multi-Format Schema Object (AsyncAPI 3, en message.payload o en una entrada de components.schemas) declara un schemaFormat de Avro — es decir, su valor contiene application/vnd.apache.avro — ese valor debe ser exactamente application/vnd.apache.avro;version=1.9.0.

+

La versión 1.9.0 es la versión estándar de Avro y aparece en todos los ejemplos de la Guía de Estilo. Cualquier otra versión, o la ausencia de versión, se reporta como incidencia. Los valores de schemaFormat que no sean de Avro (por ejemplo, un schemaFormat de JSON Schema) quedan fuera del alcance de esta regla.

+

Ejemplo Noncompliant (AsyncAPI 2)

+
+asyncapi: '2.6.0'
+info:
+  title: Carga API
+  version: '1.0.0'
+channels:
+  carga:
+    subscribe:
+      message:
+        schemaFormat: 'application/vnd.apache.avro;version=1.8.0'
+        payload:
+          type: record
+          name: CargaValue
+          namespace: com.example.avro
+          fields:
+            - name: id
+              type: string
+
+

Solución Compliant (AsyncAPI 2)

+
+asyncapi: '2.6.0'
+info:
+  title: Carga API
+  version: '1.0.0'
+channels:
+  carga:
+    subscribe:
+      message:
+        schemaFormat: 'application/vnd.apache.avro;version=1.9.0'
+        payload:
+          type: record
+          name: CargaValue
+          namespace: com.example.avro
+          fields:
+            - name: id
+              type: string
+
+

Ejemplo Noncompliant (AsyncAPI 3)

+
+asyncapi: 3.0.0
+info:
+  title: Carga API
+  version: 1.0.0
+channels:
+  carga:
+    address: carga
+    messages:
+      CargaMessage:
+        payload:
+          schemaFormat: 'application/vnd.apache.avro;version=1.8.0'
+          schema:
+            type: record
+            name: CargaValue
+            namespace: com.example.avro
+            fields:
+              - name: id
+                type: string
+
+

Solución Compliant (AsyncAPI 3)

+
+asyncapi: 3.0.0
+info:
+  title: Carga API
+  version: 1.0.0
+channels:
+  carga:
+    address: carga
+    messages:
+      CargaMessage:
+        payload:
+          schemaFormat: 'application/vnd.apache.avro;version=1.9.0'
+          schema:
+            type: record
+            name: CargaValue
+            namespace: com.example.avro
+            fields:
+              - name: id
+                type: string
+
diff --git a/src/main/resources/org/sonar/l10n/es/asyncapi/rules/asyncapi/schemas/AAR056.json b/src/main/resources/org/sonar/l10n/es/asyncapi/rules/asyncapi/schemas/AAR056.json new file mode 100644 index 0000000..6d6927d --- /dev/null +++ b/src/main/resources/org/sonar/l10n/es/asyncapi/rules/asyncapi/schemas/AAR056.json @@ -0,0 +1,13 @@ +{ + "title": "AAR056 - AvroSchemaFormat - El schemaFormat debe ser application/vnd.apache.avro;version=1.9.0", + "type": "BUG", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5mn" + }, + "tags": [ + "schemas" + ], + "defaultSeverity": "MAJOR" +} diff --git a/src/test/java/org/sonar/samples/asyncapi/checks/schemas/AAR056AvroSchemaFormatCheckTest.java b/src/test/java/org/sonar/samples/asyncapi/checks/schemas/AAR056AvroSchemaFormatCheckTest.java new file mode 100644 index 0000000..4232340 --- /dev/null +++ b/src/test/java/org/sonar/samples/asyncapi/checks/schemas/AAR056AvroSchemaFormatCheckTest.java @@ -0,0 +1,66 @@ +package org.sonar.samples.asyncapi.checks.schemas; + +import org.junit.Before; +import org.junit.Test; +import org.sonar.api.rule.Severity; +import org.sonar.api.rules.RuleType; +import org.sonar.samples.asyncapi.BaseCheckTest; + +import apiquality.sonar.asyncapi.checks.schemas.AAR056AvroSchemaFormatCheck; + +public class AAR056AvroSchemaFormatCheckTest extends BaseCheckTest { + + @Before + public void init() { + ruleName = "AAR056"; + check = new AAR056AvroSchemaFormatCheck(); + v2Path = getV2Path("schemas"); + v3Path = getV3Path("schemas"); + v31Path = getV31Path("schemas"); + } + + @Test + public void verifyV2Valid() { + verifyV2("valid.yaml"); + } + + @Test + public void verifyV2WrongVersion() { + verifyV2("wrong-version.yaml"); + } + + @Test + public void verifyV2NonAvroSchemaFormat() { + verifyV2("non-avro-schema-format.yaml"); + } + + @Test + public void verifyV3Valid() { + verifyV3("valid.yaml"); + } + + @Test + public void verifyV3WrongVersion() { + verifyV3("wrong-version.yaml"); + } + + @Test + public void verifyV3NonAvroSchemaFormat() { + verifyV3("non-avro-schema-format.yaml"); + } + + @Test + public void verifyV31Valid() { + verifyV31("valid.yaml"); + } + + @Test + public void verifyV31WrongVersion() { + verifyV31("wrong-version.yaml"); + } + + @Override + public void verifyRule() { + assertRuleProperties("AAR056 - AvroSchemaFormat - The schemaFormat must be application/vnd.apache.avro;version=1.9.0", RuleType.BUG, Severity.MAJOR, tags("schemas")); + } +} diff --git a/src/test/resources/checks/v2/schemas/AAR056/non-avro-schema-format.yaml b/src/test/resources/checks/v2/schemas/AAR056/non-avro-schema-format.yaml new file mode 100644 index 0000000..cf3f96e --- /dev/null +++ b/src/test/resources/checks/v2/schemas/AAR056/non-avro-schema-format.yaml @@ -0,0 +1,15 @@ +asyncapi: '2.6.0' +info: + title: Avro Schema Format Non Avro V2 + version: '1.0.0' +channels: + carga: + subscribe: + operationId: receiveCarga + message: + schemaFormat: 'application/schema+json;version=draft-07' + payload: + type: object + properties: + id: + type: string diff --git a/src/test/resources/checks/v2/schemas/AAR056/valid.yaml b/src/test/resources/checks/v2/schemas/AAR056/valid.yaml new file mode 100644 index 0000000..422196e --- /dev/null +++ b/src/test/resources/checks/v2/schemas/AAR056/valid.yaml @@ -0,0 +1,27 @@ +asyncapi: '2.6.0' +info: + title: Avro Schema Format Valid V2 + version: '1.0.0' +channels: + carga: + subscribe: + operationId: receiveCarga + message: + schemaFormat: 'application/vnd.apache.avro;version=1.9.0' + payload: + type: record + name: CargaValue + namespace: com.example.avro + fields: + - name: id + type: string +components: + schemas: + CargaComponent: + schemaFormat: 'application/vnd.apache.avro;version=1.9.0' + type: record + name: CargaComponent + namespace: com.example.avro + fields: + - name: id + type: string diff --git a/src/test/resources/checks/v2/schemas/AAR056/wrong-version.yaml b/src/test/resources/checks/v2/schemas/AAR056/wrong-version.yaml new file mode 100644 index 0000000..26be003 --- /dev/null +++ b/src/test/resources/checks/v2/schemas/AAR056/wrong-version.yaml @@ -0,0 +1,27 @@ +asyncapi: '2.6.0' +info: + title: Avro Schema Format Wrong Version V2 + version: '1.0.0' +channels: + carga: + subscribe: + operationId: receiveCarga + message: + schemaFormat: 'application/vnd.apache.avro;version=1.8.0' # Noncompliant {{AAR056: The 'schemaFormat' value 'application/vnd.apache.avro;version=1.8.0' must be exactly 'application/vnd.apache.avro;version=1.9.0' when the payload uses Avro}} + payload: + type: record + name: CargaValue + namespace: com.example.avro + fields: + - name: id + type: string +components: + schemas: + CargaComponent: + schemaFormat: 'application/vnd.apache.avro' # Noncompliant {{AAR056: The 'schemaFormat' value 'application/vnd.apache.avro' must be exactly 'application/vnd.apache.avro;version=1.9.0' when the payload uses Avro}} + type: record + name: CargaComponent + namespace: com.example.avro + fields: + - name: id + type: string diff --git a/src/test/resources/checks/v3/schemas/AAR056/non-avro-schema-format.yaml b/src/test/resources/checks/v3/schemas/AAR056/non-avro-schema-format.yaml new file mode 100644 index 0000000..83b7102 --- /dev/null +++ b/src/test/resources/checks/v3/schemas/AAR056/non-avro-schema-format.yaml @@ -0,0 +1,16 @@ +asyncapi: 3.0.0 +info: + title: Avro Schema Format Non Avro V3 + version: 1.0.0 +channels: + carga: + address: carga + messages: + CargaMessage: + payload: + schemaFormat: 'application/schema+json;version=draft-07' + schema: + type: object + properties: + id: + type: string diff --git a/src/test/resources/checks/v3/schemas/AAR056/valid.yaml b/src/test/resources/checks/v3/schemas/AAR056/valid.yaml new file mode 100644 index 0000000..9ea6785 --- /dev/null +++ b/src/test/resources/checks/v3/schemas/AAR056/valid.yaml @@ -0,0 +1,29 @@ +asyncapi: 3.0.0 +info: + title: Avro Schema Format Valid V3 + version: 1.0.0 +channels: + carga: + address: carga + messages: + CargaMessage: + payload: + schemaFormat: 'application/vnd.apache.avro;version=1.9.0' + schema: + type: record + name: CargaValue + namespace: com.example.avro + fields: + - name: id + type: string +components: + schemas: + CargaComponent: + schemaFormat: 'application/vnd.apache.avro;version=1.9.0' + schema: + type: record + name: CargaComponent + namespace: com.example.avro + fields: + - name: id + type: string diff --git a/src/test/resources/checks/v3/schemas/AAR056/wrong-version.yaml b/src/test/resources/checks/v3/schemas/AAR056/wrong-version.yaml new file mode 100644 index 0000000..c164c91 --- /dev/null +++ b/src/test/resources/checks/v3/schemas/AAR056/wrong-version.yaml @@ -0,0 +1,29 @@ +asyncapi: 3.0.0 +info: + title: Avro Schema Format Wrong Version V3 + version: 1.0.0 +channels: + carga: + address: carga + messages: + CargaMessage: + payload: + schemaFormat: 'application/vnd.apache.avro;version=1.8.0' # Noncompliant {{AAR056: The 'schemaFormat' value 'application/vnd.apache.avro;version=1.8.0' must be exactly 'application/vnd.apache.avro;version=1.9.0' when the payload uses Avro}} + schema: + type: record + name: CargaValue + namespace: com.example.avro + fields: + - name: id + type: string +components: + schemas: + CargaComponent: + schemaFormat: 'application/vnd.apache.avro' # Noncompliant {{AAR056: The 'schemaFormat' value 'application/vnd.apache.avro' must be exactly 'application/vnd.apache.avro;version=1.9.0' when the payload uses Avro}} + schema: + type: record + name: CargaComponent + namespace: com.example.avro + fields: + - name: id + type: string diff --git a/src/test/resources/checks/v31/schemas/AAR056/valid.yaml b/src/test/resources/checks/v31/schemas/AAR056/valid.yaml new file mode 100644 index 0000000..06e3d39 --- /dev/null +++ b/src/test/resources/checks/v31/schemas/AAR056/valid.yaml @@ -0,0 +1,18 @@ +asyncapi: 3.1.0 +info: + title: Avro Schema Format Valid V31 + version: 1.0.0 +channels: + carga: + address: carga + messages: + CargaMessage: + payload: + schemaFormat: 'application/vnd.apache.avro;version=1.9.0' + schema: + type: record + name: CargaValue + namespace: com.example.avro + fields: + - name: id + type: string diff --git a/src/test/resources/checks/v31/schemas/AAR056/wrong-version.yaml b/src/test/resources/checks/v31/schemas/AAR056/wrong-version.yaml new file mode 100644 index 0000000..c3c8ce2 --- /dev/null +++ b/src/test/resources/checks/v31/schemas/AAR056/wrong-version.yaml @@ -0,0 +1,18 @@ +asyncapi: 3.1.0 +info: + title: Avro Schema Format Wrong Version V31 + version: 1.0.0 +channels: + carga: + address: carga + messages: + CargaMessage: + payload: + schemaFormat: 'application/vnd.apache.avro;version=1.8.0' # Noncompliant {{AAR056: The 'schemaFormat' value 'application/vnd.apache.avro;version=1.8.0' must be exactly 'application/vnd.apache.avro;version=1.9.0' when the payload uses Avro}} + schema: + type: record + name: CargaValue + namespace: com.example.avro + fields: + - name: id + type: string