This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch fix/CAMEL-24482 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 40105f23e006573397a94af2b36e361424024933 Author: Claus Ibsen <[email protected]> AuthorDate: Thu Aug 27 18:52:43 2026 +0200 CAMEL-24482: camel-yaml-dsl - Canonical schema wrongly requires every data format on unmarshal The canonical YAML DSL JSON schema disables the oneOf grouping used to express "pick exactly one of N alternatives" (GenerateYamlSchemaMojo's isInOneOf is always false in canonical mode). Since each unmarshal data format alternative also carries required=true (needed to enforce the "one of N" cardinality in the non-canonical, oneOf-based schema), that required flag leaked straight into the flat top-level "required" list, turning "choose one of 50 data formats" into "all 50 are mandatory". marshal.dataFormatType lacked the same required=true annotation, so it accidentally avoided the bug while also silently accepting a marshal step with no data format at all (a route that fails at build time with "type must be specified"). Align it with unmarshal so both enforce the same "exactly one data format" contract. Since a flat, oneOf-free canonical schema cannot itself express "exactly one of N" cardinality, YamlValidator now re-checks it directly, driven by the same "oneOf"/"required" metadata already published in the Camel catalog's EIP model JSON - covering not just data formats, but every "pick one" option group (expression languages, error handlers, load balancers, resequencer config, tokenizers) without hardcoding any of their alternative names. Co-Authored-By: Claude Sonnet 5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../camel/catalog/schemas/camelYamlDsl-model.json | 158 +++++++-------------- .../org/apache/camel/model/MarshalDefinition.java | 3 +- .../dsl/yaml/deserializers/ModelDeserializers.java | 100 ++++++------- .../maven/dsl/yaml/GenerateYamlSchemaMojo.java | 7 +- .../camel-yaml-dsl-validator/pom.xml | 4 + .../camel/dsl/yaml/validator/YamlValidator.java | 123 +++++++++++++++- .../yaml/validator/YamlCanonicalValidatorTest.java | 66 +++++++++ .../canonical-invalid-missing-dataformat.yaml | 22 +++ .../test/resources/canonical-valid-dataformat.yaml | 27 ++++ .../resources/schema/camelYamlDsl-canonical.json | 8 +- .../resources/schema/camelYamlDsl-model.json | 158 +++++++-------------- .../generated/resources/schema/camelYamlDsl.json | 104 -------------- .../org/apache/camel/dsl/yaml/MarshalTest.groovy | 12 +- 13 files changed, 417 insertions(+), 375 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json index 80f73eaa4b1e..9a683bb8bf48 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json @@ -5301,11 +5301,11 @@ "ref" : "expression", "description" : "The property value as an expression.", "title" : "Expression", - "required" : true, "displayName" : "Expression", "kind" : "expression", "index" : 1, - "group" : "common" + "group" : "common", + "required" : true } ] }, "recipientList" : { @@ -6154,11 +6154,11 @@ "ref" : "expression", "description" : "Expression to use for re-ordering the messages, such as a header with a sequence number.", "title" : "Expression", - "required" : true, "displayName" : "Expression", "kind" : "expression", "index" : 4, - "group" : "common" + "group" : "common", + "required" : true }, { "name" : "batchConfig", "type" : "object", @@ -9401,253 +9401,203 @@ }, { "name" : "asn1", "type" : "object", - "ref" : "aSN1", - "required" : true + "ref" : "aSN1" }, { "name" : "avro", "type" : "object", - "ref" : "avro", - "required" : true + "ref" : "avro" }, { "name" : "barcode", "type" : "object", - "ref" : "barcode", - "required" : true + "ref" : "barcode" }, { "name" : "base64", "type" : "object", - "ref" : "base64", - "required" : true + "ref" : "base64" }, { "name" : "beanio", "type" : "object", - "ref" : "beanio", - "required" : true + "ref" : "beanio" }, { "name" : "bindy", "type" : "object", - "ref" : "bindy", - "required" : true + "ref" : "bindy" }, { "name" : "cbor", "type" : "object", - "ref" : "cBOR", - "required" : true + "ref" : "cBOR" }, { "name" : "crypto", "type" : "object", - "ref" : "crypto", - "required" : true + "ref" : "crypto" }, { "name" : "csv", "type" : "object", - "ref" : "csv", - "required" : true + "ref" : "csv" }, { "name" : "custom", "type" : "object", - "ref" : "custom", - "required" : true + "ref" : "custom" }, { "name" : "dfdl", "type" : "object", - "ref" : "dfdl", - "required" : true + "ref" : "dfdl" }, { "name" : "fhirJson", "type" : "object", - "ref" : "fhirJson", - "required" : true + "ref" : "fhirJson" }, { "name" : "fhirXml", "type" : "object", - "ref" : "fhirXml", - "required" : true + "ref" : "fhirXml" }, { "name" : "flatpack", "type" : "object", - "ref" : "flatpack", - "required" : true + "ref" : "flatpack" }, { "name" : "fory", "type" : "object", - "ref" : "fory", - "required" : true + "ref" : "fory" }, { "name" : "grok", "type" : "object", - "ref" : "grok", - "required" : true + "ref" : "grok" }, { "name" : "groovyJson", "type" : "object", - "ref" : "groovyJSon", - "required" : true + "ref" : "groovyJSon" }, { "name" : "groovyXml", "type" : "object", - "ref" : "groovyXml", - "required" : true + "ref" : "groovyXml" }, { "name" : "gzipDeflater", "type" : "object", - "ref" : "gzipDeflater", - "required" : true + "ref" : "gzipDeflater" }, { "name" : "hl7", "type" : "object", - "ref" : "hL7", - "required" : true + "ref" : "hL7" }, { "name" : "ical", "type" : "object", - "ref" : "ical", - "required" : true + "ref" : "ical" }, { "name" : "iso8583", "type" : "object", - "ref" : "iso8583", - "required" : true + "ref" : "iso8583" }, { "name" : "jacksonXml", "type" : "object", - "ref" : "jacksonXML", - "required" : true + "ref" : "jacksonXML" }, { "name" : "jaxb", "type" : "object", - "ref" : "jaxb", - "required" : true + "ref" : "jaxb" }, { "name" : "json", "type" : "object", - "ref" : "json", - "required" : true + "ref" : "json" }, { "name" : "jsonApi", "type" : "object", - "ref" : "jsonApi", - "required" : true + "ref" : "jsonApi" }, { "name" : "lzf", "type" : "object", - "ref" : "lZF", - "required" : true + "ref" : "lZF" }, { "name" : "mimeMultipart", "type" : "object", - "ref" : "mimeMultipart", - "required" : true + "ref" : "mimeMultipart" }, { "name" : "ocsf", "type" : "object", - "ref" : "ocsf", - "required" : true + "ref" : "ocsf" }, { "name" : "parquetAvro", "type" : "object", - "ref" : "parquetAvro", - "required" : true + "ref" : "parquetAvro" }, { "name" : "pgp", "type" : "object", - "ref" : "pGP", - "required" : true + "ref" : "pGP" }, { "name" : "pqc", "type" : "object", - "ref" : "pQC", - "required" : true + "ref" : "pQC" }, { "name" : "protobuf", "type" : "object", - "ref" : "protobuf", - "required" : true + "ref" : "protobuf" }, { "name" : "rss", "type" : "object", - "ref" : "rss", - "required" : true + "ref" : "rss" }, { "name" : "smooks", "type" : "object", - "ref" : "smooks", - "required" : true + "ref" : "smooks" }, { "name" : "soap", "type" : "object", - "ref" : "soap", - "required" : true + "ref" : "soap" }, { "name" : "swiftMt", "type" : "object", - "ref" : "swiftMt", - "required" : true + "ref" : "swiftMt" }, { "name" : "swiftMx", "type" : "object", - "ref" : "swiftMx", - "required" : true + "ref" : "swiftMx" }, { "name" : "syslog", "type" : "object", - "ref" : "syslog", - "required" : true + "ref" : "syslog" }, { "name" : "tarFile", "type" : "object", - "ref" : "tarFile", - "required" : true + "ref" : "tarFile" }, { "name" : "thrift", "type" : "object", - "ref" : "thrift", - "required" : true + "ref" : "thrift" }, { "name" : "toon", "type" : "object", - "ref" : "toon", - "required" : true + "ref" : "toon" }, { "name" : "ubl", "type" : "object", - "ref" : "ubl", - "required" : true + "ref" : "ubl" }, { "name" : "univocityCsv", "type" : "object", - "ref" : "uniVocityCsv", - "required" : true + "ref" : "uniVocityCsv" }, { "name" : "univocityFixed", "type" : "object", - "ref" : "uniVocityFixed", - "required" : true + "ref" : "uniVocityFixed" }, { "name" : "univocityTsv", "type" : "object", - "ref" : "uniVocityTsv", - "required" : true + "ref" : "uniVocityTsv" }, { "name" : "xmlSecurity", "type" : "object", - "ref" : "xMLSecurity", - "required" : true + "ref" : "xMLSecurity" }, { "name" : "yaml", "type" : "object", - "ref" : "yAML", - "required" : true + "ref" : "yAML" }, { "name" : "zipDeflater", "type" : "object", - "ref" : "zipDeflater", - "required" : true + "ref" : "zipDeflater" }, { "name" : "zipFile", "type" : "object", - "ref" : "zipFile", - "required" : true + "ref" : "zipFile" } ] }, "validate" : { diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/MarshalDefinition.java b/core/camel-core-model/src/main/java/org/apache/camel/model/MarshalDefinition.java index 0809bb2ff669..806d55f742ab 100644 --- a/core/camel-core-model/src/main/java/org/apache/camel/model/MarshalDefinition.java +++ b/core/camel-core-model/src/main/java/org/apache/camel/model/MarshalDefinition.java @@ -136,7 +136,8 @@ public class MarshalDefinition extends NoOutputDefinition<MarshalDefinition> imp @XmlElement(name = "yaml", type = YAMLDataFormat.class), @XmlElement(name = "zipDeflater", type = ZipDeflaterDataFormat.class), @XmlElement(name = "zipFile", type = ZipFileDataFormat.class) }) - @Metadata(description = "The data format to use for marshalling the message body into a specific format such as JSON, XML, CSV, Avro, Protobuf, etc.") + @Metadata(required = true, + description = "The data format to use for marshalling the message body into a specific format such as JSON, XML, CSV, Avro, Protobuf, etc.") private DataFormatDefinition dataFormatType; @XmlAttribute @Metadata(description = "To use a variable as the source for the message body to send. This makes it handy to use variables for user data and to easily control what data to use for sending and receiving.") diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/generated/java/org/apache/camel/dsl/yaml/deserializers/ModelDeserializers.java b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/generated/java/org/apache/camel/dsl/yaml/deserializers/ModelDeserializers.java index 4599b4fde0a0..1535f6c08263 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/generated/java/org/apache/camel/dsl/yaml/deserializers/ModelDeserializers.java +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/generated/java/org/apache/camel/dsl/yaml/deserializers/ModelDeserializers.java @@ -9461,62 +9461,62 @@ public final class ModelDeserializers extends YamlDeserializerSupport { description = "Serializes the message body into a specific data format such as JSON, XML, CSV, or Protobuf for transmission or storage", deprecated = false, properties = { - @YamlProperty(name = "asn1", type = "object:org.apache.camel.model.dataformat.ASN1DataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "avro", type = "object:org.apache.camel.model.dataformat.AvroDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "barcode", type = "object:org.apache.camel.model.dataformat.BarcodeDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "base64", type = "object:org.apache.camel.model.dataformat.Base64DataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "beanio", type = "object:org.apache.camel.model.dataformat.BeanioDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "bindy", type = "object:org.apache.camel.model.dataformat.BindyDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "cbor", type = "object:org.apache.camel.model.dataformat.CBORDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "crypto", type = "object:org.apache.camel.model.dataformat.CryptoDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "csv", type = "object:org.apache.camel.model.dataformat.CsvDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "custom", type = "object:org.apache.camel.model.dataformat.CustomDataFormat", oneOf = "dataFormatType"), + @YamlProperty(name = "asn1", type = "object:org.apache.camel.model.dataformat.ASN1DataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "avro", type = "object:org.apache.camel.model.dataformat.AvroDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "barcode", type = "object:org.apache.camel.model.dataformat.BarcodeDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "base64", type = "object:org.apache.camel.model.dataformat.Base64DataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "beanio", type = "object:org.apache.camel.model.dataformat.BeanioDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "bindy", type = "object:org.apache.camel.model.dataformat.BindyDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "cbor", type = "object:org.apache.camel.model.dataformat.CBORDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "crypto", type = "object:org.apache.camel.model.dataformat.CryptoDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "csv", type = "object:org.apache.camel.model.dataformat.CsvDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "custom", type = "object:org.apache.camel.model.dataformat.CustomDataFormat", required = true, oneOf = "dataFormatType"), @YamlProperty(name = "description", type = "string", description = "The description for this node", displayName = "Description"), - @YamlProperty(name = "dfdl", type = "object:org.apache.camel.model.dataformat.DfdlDataFormat", oneOf = "dataFormatType"), + @YamlProperty(name = "dfdl", type = "object:org.apache.camel.model.dataformat.DfdlDataFormat", required = true, oneOf = "dataFormatType"), @YamlProperty(name = "disabled", type = "boolean", defaultValue = "false", description = "Whether to disable this EIP from the route during build time. Once an EIP has been disabled then it cannot be enabled later at runtime.", displayName = "Disabled"), - @YamlProperty(name = "fhirJson", type = "object:org.apache.camel.model.dataformat.FhirJsonDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "fhirXml", type = "object:org.apache.camel.model.dataformat.FhirXmlDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "flatpack", type = "object:org.apache.camel.model.dataformat.FlatpackDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "fory", type = "object:org.apache.camel.model.dataformat.ForyDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "grok", type = "object:org.apache.camel.model.dataformat.GrokDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "groovyJson", type = "object:org.apache.camel.model.dataformat.GroovyJSonDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "groovyXml", type = "object:org.apache.camel.model.dataformat.GroovyXmlDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "gzipDeflater", type = "object:org.apache.camel.model.dataformat.GzipDeflaterDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "hl7", type = "object:org.apache.camel.model.dataformat.HL7DataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "ical", type = "object:org.apache.camel.model.dataformat.IcalDataFormat", oneOf = "dataFormatType"), + @YamlProperty(name = "fhirJson", type = "object:org.apache.camel.model.dataformat.FhirJsonDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "fhirXml", type = "object:org.apache.camel.model.dataformat.FhirXmlDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "flatpack", type = "object:org.apache.camel.model.dataformat.FlatpackDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "fory", type = "object:org.apache.camel.model.dataformat.ForyDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "grok", type = "object:org.apache.camel.model.dataformat.GrokDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "groovyJson", type = "object:org.apache.camel.model.dataformat.GroovyJSonDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "groovyXml", type = "object:org.apache.camel.model.dataformat.GroovyXmlDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "gzipDeflater", type = "object:org.apache.camel.model.dataformat.GzipDeflaterDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "hl7", type = "object:org.apache.camel.model.dataformat.HL7DataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "ical", type = "object:org.apache.camel.model.dataformat.IcalDataFormat", required = true, oneOf = "dataFormatType"), @YamlProperty(name = "id", type = "string", description = "The id of this node", displayName = "Id"), - @YamlProperty(name = "iso8583", type = "object:org.apache.camel.model.dataformat.Iso8583DataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "jacksonXml", type = "object:org.apache.camel.model.dataformat.JacksonXMLDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "jaxb", type = "object:org.apache.camel.model.dataformat.JaxbDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "json", type = "object:org.apache.camel.model.dataformat.JsonDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "jsonApi", type = "object:org.apache.camel.model.dataformat.JsonApiDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "lzf", type = "object:org.apache.camel.model.dataformat.LZFDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "mimeMultipart", type = "object:org.apache.camel.model.dataformat.MimeMultipartDataFormat", oneOf = "dataFormatType"), + @YamlProperty(name = "iso8583", type = "object:org.apache.camel.model.dataformat.Iso8583DataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "jacksonXml", type = "object:org.apache.camel.model.dataformat.JacksonXMLDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "jaxb", type = "object:org.apache.camel.model.dataformat.JaxbDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "json", type = "object:org.apache.camel.model.dataformat.JsonDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "jsonApi", type = "object:org.apache.camel.model.dataformat.JsonApiDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "lzf", type = "object:org.apache.camel.model.dataformat.LZFDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "mimeMultipart", type = "object:org.apache.camel.model.dataformat.MimeMultipartDataFormat", required = true, oneOf = "dataFormatType"), @YamlProperty(name = "note", type = "string", description = "The note for this node", displayName = "Note"), - @YamlProperty(name = "ocsf", type = "object:org.apache.camel.model.dataformat.OcsfDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "parquetAvro", type = "object:org.apache.camel.model.dataformat.ParquetAvroDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "pgp", type = "object:org.apache.camel.model.dataformat.PGPDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "pqc", type = "object:org.apache.camel.model.dataformat.PQCDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "protobuf", type = "object:org.apache.camel.model.dataformat.ProtobufDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "rss", type = "object:org.apache.camel.model.dataformat.RssDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "smooks", type = "object:org.apache.camel.model.dataformat.SmooksDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "soap", type = "object:org.apache.camel.model.dataformat.SoapDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "swiftMt", type = "object:org.apache.camel.model.dataformat.SwiftMtDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "swiftMx", type = "object:org.apache.camel.model.dataformat.SwiftMxDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "syslog", type = "object:org.apache.camel.model.dataformat.SyslogDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "tarFile", type = "object:org.apache.camel.model.dataformat.TarFileDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "thrift", type = "object:org.apache.camel.model.dataformat.ThriftDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "toon", type = "object:org.apache.camel.model.dataformat.ToonDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "ubl", type = "object:org.apache.camel.model.dataformat.UblDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "univocityCsv", type = "object:org.apache.camel.model.dataformat.UniVocityCsvDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "univocityFixed", type = "object:org.apache.camel.model.dataformat.UniVocityFixedDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "univocityTsv", type = "object:org.apache.camel.model.dataformat.UniVocityTsvDataFormat", oneOf = "dataFormatType"), + @YamlProperty(name = "ocsf", type = "object:org.apache.camel.model.dataformat.OcsfDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "parquetAvro", type = "object:org.apache.camel.model.dataformat.ParquetAvroDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "pgp", type = "object:org.apache.camel.model.dataformat.PGPDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "pqc", type = "object:org.apache.camel.model.dataformat.PQCDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "protobuf", type = "object:org.apache.camel.model.dataformat.ProtobufDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "rss", type = "object:org.apache.camel.model.dataformat.RssDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "smooks", type = "object:org.apache.camel.model.dataformat.SmooksDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "soap", type = "object:org.apache.camel.model.dataformat.SoapDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "swiftMt", type = "object:org.apache.camel.model.dataformat.SwiftMtDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "swiftMx", type = "object:org.apache.camel.model.dataformat.SwiftMxDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "syslog", type = "object:org.apache.camel.model.dataformat.SyslogDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "tarFile", type = "object:org.apache.camel.model.dataformat.TarFileDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "thrift", type = "object:org.apache.camel.model.dataformat.ThriftDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "toon", type = "object:org.apache.camel.model.dataformat.ToonDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "ubl", type = "object:org.apache.camel.model.dataformat.UblDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "univocityCsv", type = "object:org.apache.camel.model.dataformat.UniVocityCsvDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "univocityFixed", type = "object:org.apache.camel.model.dataformat.UniVocityFixedDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "univocityTsv", type = "object:org.apache.camel.model.dataformat.UniVocityTsvDataFormat", required = true, oneOf = "dataFormatType"), @YamlProperty(name = "variableReceive", type = "string", description = "To use a variable to store the received message body (only body, not headers). This makes it handy to use variables for user data and to easily control what data to use for sending and receiving.", displayName = "Variable Receive"), @YamlProperty(name = "variableSend", type = "string", description = "To use a variable as the source for the message body to send. This makes it handy to use variables for user data and to easily control what data to use for sending and receiving.", displayName = "Variable Send"), - @YamlProperty(name = "xmlSecurity", type = "object:org.apache.camel.model.dataformat.XMLSecurityDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "yaml", type = "object:org.apache.camel.model.dataformat.YAMLDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "zipDeflater", type = "object:org.apache.camel.model.dataformat.ZipDeflaterDataFormat", oneOf = "dataFormatType"), - @YamlProperty(name = "zipFile", type = "object:org.apache.camel.model.dataformat.ZipFileDataFormat", oneOf = "dataFormatType") + @YamlProperty(name = "xmlSecurity", type = "object:org.apache.camel.model.dataformat.XMLSecurityDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "yaml", type = "object:org.apache.camel.model.dataformat.YAMLDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "zipDeflater", type = "object:org.apache.camel.model.dataformat.ZipDeflaterDataFormat", required = true, oneOf = "dataFormatType"), + @YamlProperty(name = "zipFile", type = "object:org.apache.camel.model.dataformat.ZipFileDataFormat", required = true, oneOf = "dataFormatType") } ) public static class MarshalDefinitionDeserializer extends YamlDeserializerBase<MarshalDefinition> { diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl-maven-plugin/src/main/java/org/apache/camel/maven/dsl/yaml/GenerateYamlSchemaMojo.java b/dsl/camel-yaml-dsl/camel-yaml-dsl-maven-plugin/src/main/java/org/apache/camel/maven/dsl/yaml/GenerateYamlSchemaMojo.java index 2fd42f59817f..914e3ef37ac8 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl-maven-plugin/src/main/java/org/apache/camel/maven/dsl/yaml/GenerateYamlSchemaMojo.java +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl-maven-plugin/src/main/java/org/apache/camel/maven/dsl/yaml/GenerateYamlSchemaMojo.java @@ -345,7 +345,12 @@ public class GenerateYamlSchemaMojo extends GenerateYamlSupportMojo { propertyWrapItem, additionalProperties); - if (propertyRequired) { + // A property that belongs to a oneOf group (e.g. the data formats of marshal/unmarshal, or the + // languages of an expression) is only required as part of choosing one of the alternatives, not + // unconditionally. In canonical mode the oneOf grouping itself is dropped (isInOneOf is always + // false), so without this guard every alternative would end up in the flat "required" list, + // demanding all of them at once instead of exactly one. + if (propertyRequired && StringUtils.isEmpty(propertyOneOf)) { definition.withArray("required").add(propertyName); } } diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/pom.xml b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/pom.xml index ca8fa7c84c31..4392c785b763 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/pom.xml +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/pom.xml @@ -54,6 +54,10 @@ <groupId>org.apache.camel</groupId> <artifactId>camel-stub</artifactId> </dependency> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-catalog</artifactId> + </dependency> <dependency> <groupId>com.networknt</groupId> <artifactId>json-schema-validator</artifactId> diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/YamlValidator.java b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/YamlValidator.java index efc833acb421..5b10bcc80123 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/YamlValidator.java +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/YamlValidator.java @@ -20,6 +20,8 @@ import java.io.File; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -40,6 +42,11 @@ import com.networknt.schema.SpecificationVersion; import com.networknt.schema.dialect.Dialect; import com.networknt.schema.dialect.Dialects; import com.networknt.schema.keyword.NonValidationKeyword; +import com.networknt.schema.path.NodePath; +import com.networknt.schema.path.PathType; +import org.apache.camel.catalog.CamelCatalog; +import org.apache.camel.catalog.DefaultCamelCatalog; +import org.apache.camel.tooling.model.EipModel; /** * YAML DSL validator that tooling can use to validate Camel source files if they can be parsed and are valid according @@ -50,9 +57,27 @@ public class YamlValidator { private static final String LOCATION = "/schema/camelYamlDsl.json"; private static final String LOCATION_CANONICAL = "/schema/camelYamlDsl-canonical.json"; + /** + * A handful of "pick exactly one" EIP option groups (see {@link EipModel.EipOptionModel#getOneOfs()}) flatten their + * alternatives directly onto a specific host node instead of appearing under a wrapper key named after the option + * itself (that's how "expression" works). The canonical schema cannot express this "exactly one of" cardinality (it + * has no oneOf/anyOf constructs), so {@link #checkOneOfCardinality} re-checks it here, driven by the same catalog + * metadata the classic schema is generated from. + */ + private static final Map<String, Set<String>> FLATTENED_HOSTS = Map.of( + "dataFormatType", Set.of("marshal", "unmarshal"), + "errorHandlerType", Set.of("errorHandler"), + "tokenizerImplementation", Set.of("tokenizer"), + "resequencerConfig", Set.of("resequence"), + "loadBalancerType", Set.of("loadBalance")); + private final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); private final boolean canonical; private Schema schema; + private Map<String, OneOfGroup> oneOfGroups; + + private record OneOfGroup(Set<String> alternatives, boolean required) { + } public YamlValidator() { this(false); @@ -72,7 +97,7 @@ public class YamlValidator { } try { var target = mapper.readTree(file); - return filterOneOfNoise(new ArrayList<>(schema.validate(target))); + return validate(target); } catch (Exception e) { return List.of(parseError(e)); } @@ -84,12 +109,20 @@ public class YamlValidator { } try { var target = mapper.readTree(content); - return filterOneOfNoise(new ArrayList<>(schema.validate(target))); + return validate(target); } catch (Exception e) { return List.of(parseError(e)); } } + private List<Error> validate(JsonNode target) { + var errors = filterOneOfNoise(new ArrayList<>(schema.validate(target))); + if (canonical) { + checkOneOfCardinality(target, new NodePath(PathType.JSON_POINTER), errors); + } + return errors; + } + /** * Filters noise from {@code oneOf} validation. When a {@code oneOf} has N branches and none match, the validator * reports errors from ALL branches — producing dozens of "required property 'X' not found" messages for branches @@ -221,6 +254,88 @@ public class YamlValidator { || "maxItems".equals(keyword)); } + /** + * Recursively walks the parsed YAML tree looking for the host nodes/wrapper keys of a "pick exactly one" option + * group, and reports a synthetic error when zero (for a required group) or more than one alternative is present. + */ + private void checkOneOfCardinality(JsonNode node, NodePath path, List<Error> errors) { + if (node.isObject()) { + Iterator<Map.Entry<String, JsonNode>> it = node.fields(); + while (it.hasNext()) { + Map.Entry<String, JsonNode> entry = it.next(); + String key = entry.getKey(); + JsonNode value = entry.getValue(); + NodePath childPath = path.append(key); + if (value.isObject()) { + for (Map.Entry<String, Set<String>> hostEntry : FLATTENED_HOSTS.entrySet()) { + if (hostEntry.getValue().contains(key)) { + reportIfInvalidCardinality(value, oneOfGroups.get(hostEntry.getKey()), childPath, errors); + } + } + if (!FLATTENED_HOSTS.containsKey(key)) { + reportIfInvalidCardinality(value, oneOfGroups.get(key), childPath, errors); + } + } + checkOneOfCardinality(value, childPath, errors); + } + } else if (node.isArray()) { + for (int i = 0; i < node.size(); i++) { + checkOneOfCardinality(node.get(i), path.append(i), errors); + } + } + } + + private static void reportIfInvalidCardinality(JsonNode container, OneOfGroup group, NodePath path, List<Error> errors) { + if (group == null) { + return; + } + List<String> found = new ArrayList<>(); + Iterator<String> names = container.fieldNames(); + while (names.hasNext()) { + String name = names.next(); + if (group.alternatives().contains(name)) { + found.add(name); + } + } + if (found.isEmpty() && group.required()) { + errors.add(Error.builder() + .keyword("oneOf") + .instanceLocation(path) + .message("must have exactly one of " + group.alternatives() + " but found none") + .build()); + } else if (found.size() > 1) { + errors.add(Error.builder() + .keyword("oneOf") + .instanceLocation(path) + .message("must have exactly one of " + group.alternatives() + " but found: " + found) + .build()); + } + } + + /** + * Builds the "pick exactly one" option groups from the Camel catalog's EIP model metadata - the same metadata the + * classic (non-canonical) schema's oneOf groups are generated from. Only object-typed options are considered; + * array-typed options (e.g. "outputs") use "oneOf" to mean "each element is one of these types", not "exactly one + * of these sibling keys must be present". + */ + private static Map<String, OneOfGroup> loadOneOfGroups() { + Map<String, OneOfGroup> groups = new HashMap<>(); + CamelCatalog catalog = new DefaultCamelCatalog(); + for (String name : catalog.findModelNames()) { + EipModel model = catalog.eipModel(name); + if (model == null) { + continue; + } + for (EipModel.EipOptionModel option : model.getOptions()) { + List<String> oneOfs = option.getOneOfs(); + if (oneOfs != null && !oneOfs.isEmpty() && "object".equals(option.getType())) { + groups.putIfAbsent(option.getName(), new OneOfGroup(new LinkedHashSet<>(oneOfs), option.isRequired())); + } + } + } + return groups; + } + private static Error parseError(Exception e) { String msg = e.getClass().getName() + ": " + e.getMessage(); return Error.builder() @@ -248,6 +363,10 @@ public class YamlValidator { // Use a proper URI for the schema location to ensure $ref resolution works var schemaLocation = SchemaLocation.of(location); schema = schemaRegistry.getSchema(schemaLocation, model); + + if (canonical) { + oneOfGroups = loadOneOfGroups(); + } } private static Dialect getBaseDialect(SpecificationVersion version) { diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlCanonicalValidatorTest.java b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlCanonicalValidatorTest.java index 0c5578139dc4..45c5c17de9e3 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlCanonicalValidatorTest.java +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlCanonicalValidatorTest.java @@ -22,6 +22,8 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + public class YamlCanonicalValidatorTest { private static YamlValidator canonicalValidator; @@ -96,4 +98,68 @@ public class YamlCanonicalValidatorTest { Assertions.assertFalse(canonicalReport.isEmpty(), "foo.yaml uses implicit forms and should fail canonical validation"); } + + @Test + public void testUnmarshalMarshalWithSingleDataFormatPassesCanonicalValidation() throws Exception { + // CAMEL-24482: a single data format (e.g. json) on unmarshal/marshal must not require + // every other data format to also be present. + var report = canonicalValidator.validate(new File("src/test/resources/canonical-valid-dataformat.yaml")); + assertThat(report).as("Single data format should pass canonical validation but got: %s", report).isEmpty(); + } + + @Test + public void testUnmarshalMarshalWithSingleDataFormatPassesClassicValidation() throws Exception { + var report = classicValidator.validate(new File("src/test/resources/canonical-valid-dataformat.yaml")); + assertThat(report).as("Single data format should pass classic validation but got: %s", report).isEmpty(); + } + + @Test + public void testUnmarshalWithoutDataFormatFailsCanonicalValidation() throws Exception { + // The canonical schema itself has no oneOf/anyOf constructs to express "exactly one of these N + // properties is required", so YamlValidator re-checks this cardinality itself, driven by the same + // catalog "oneOf" metadata the classic schema is generated from. + var report = canonicalValidator.validate(new File("src/test/resources/canonical-invalid-missing-dataformat.yaml")); + assertThat(report).as("unmarshal without a data format should fail canonical validation").isNotEmpty(); + } + + @Test + public void testUnmarshalWithoutDataFormatFailsClassicValidation() throws Exception { + // Unlike canonical mode, the classic schema keeps its oneOf group and still requires exactly + // one data format to be chosen. + var report = classicValidator.validate(new File("src/test/resources/canonical-invalid-missing-dataformat.yaml")); + assertThat(report).as("unmarshal without a data format should fail classic validation").isNotEmpty(); + } + + @Test + public void testResequenceWithoutBatchOrStreamConfigFailsCanonicalValidation() throws Exception { + // The "exactly one of" cardinality check is generic, not special-cased to marshal/unmarshal: + // resequence must pick exactly one of batchConfig/streamConfig too. + var route = """ + - route: + from: + uri: "direct:start" + steps: + - resequence: + expression: + simple: + expression: "${header.seqnum}" + """; + var report = canonicalValidator.validate(route); + assertThat(report).as("resequence without batchConfig/streamConfig should fail canonical validation").isNotEmpty(); + } + + @Test + public void testLoadBalanceWithTwoLoadBalancersFailsCanonicalValidation() throws Exception { + var route = """ + - route: + from: + uri: "direct:start" + steps: + - loadBalance: + roundRobinLoadBalancer: {} + randomLoadBalancer: {} + """; + var report = canonicalValidator.validate(route); + assertThat(report).as("loadBalance with two load balancers should fail canonical validation").isNotEmpty(); + } } diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/resources/canonical-invalid-missing-dataformat.yaml b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/resources/canonical-invalid-missing-dataformat.yaml new file mode 100644 index 000000000000..0fb0c7cd2f19 --- /dev/null +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/resources/canonical-invalid-missing-dataformat.yaml @@ -0,0 +1,22 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +- route: + from: + uri: direct:start + steps: + - unmarshal: {} diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/resources/canonical-valid-dataformat.yaml b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/resources/canonical-valid-dataformat.yaml new file mode 100644 index 000000000000..ccad9e74c408 --- /dev/null +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/resources/canonical-valid-dataformat.yaml @@ -0,0 +1,27 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +- route: + from: + uri: direct:start + steps: + - unmarshal: + json: + id: json-unmarshal + - marshal: + json: + id: json-marshal diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json index 5a117863f229..17dde1a7adf3 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json @@ -3115,7 +3115,7 @@ "description" : "The property key." } }, - "required" : [ "expression", "key" ] + "required" : [ "key" ] }, "org.apache.camel.model.RecipientListDefinition" : { "title" : "Recipient List", @@ -3627,8 +3627,7 @@ "streamConfig" : { "$ref" : "#/items/definitions/org.apache.camel.model.config.StreamResequencerConfig" } - }, - "required" : [ "expression" ] + } }, "org.apache.camel.model.Resilience4jConfigurationDefinition" : { "title" : "Resilience4j Configuration", @@ -5767,8 +5766,7 @@ "zipFile" : { "$ref" : "#/items/definitions/org.apache.camel.model.dataformat.ZipFileDataFormat" } - }, - "required" : [ "asn1", "avro", "barcode", "base64", "beanio", "bindy", "cbor", "crypto", "csv", "custom", "dfdl", "fhirJson", "fhirXml", "flatpack", "fory", "grok", "groovyJson", "groovyXml", "gzipDeflater", "hl7", "ical", "iso8583", "jacksonXml", "jaxb", "json", "jsonApi", "lzf", "mimeMultipart", "ocsf", "parquetAvro", "pgp", "pqc", "protobuf", "rss", "smooks", "soap", "swiftMt", "swiftMx", "syslog", "tarFile", "thrift", "toon", "ubl", "univocityCsv", "univocityFixed", "univocit [...] + } }, "org.apache.camel.model.ValidateDefinition" : { "title" : "Validate", diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json index 80f73eaa4b1e..9a683bb8bf48 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json @@ -5301,11 +5301,11 @@ "ref" : "expression", "description" : "The property value as an expression.", "title" : "Expression", - "required" : true, "displayName" : "Expression", "kind" : "expression", "index" : 1, - "group" : "common" + "group" : "common", + "required" : true } ] }, "recipientList" : { @@ -6154,11 +6154,11 @@ "ref" : "expression", "description" : "Expression to use for re-ordering the messages, such as a header with a sequence number.", "title" : "Expression", - "required" : true, "displayName" : "Expression", "kind" : "expression", "index" : 4, - "group" : "common" + "group" : "common", + "required" : true }, { "name" : "batchConfig", "type" : "object", @@ -9401,253 +9401,203 @@ }, { "name" : "asn1", "type" : "object", - "ref" : "aSN1", - "required" : true + "ref" : "aSN1" }, { "name" : "avro", "type" : "object", - "ref" : "avro", - "required" : true + "ref" : "avro" }, { "name" : "barcode", "type" : "object", - "ref" : "barcode", - "required" : true + "ref" : "barcode" }, { "name" : "base64", "type" : "object", - "ref" : "base64", - "required" : true + "ref" : "base64" }, { "name" : "beanio", "type" : "object", - "ref" : "beanio", - "required" : true + "ref" : "beanio" }, { "name" : "bindy", "type" : "object", - "ref" : "bindy", - "required" : true + "ref" : "bindy" }, { "name" : "cbor", "type" : "object", - "ref" : "cBOR", - "required" : true + "ref" : "cBOR" }, { "name" : "crypto", "type" : "object", - "ref" : "crypto", - "required" : true + "ref" : "crypto" }, { "name" : "csv", "type" : "object", - "ref" : "csv", - "required" : true + "ref" : "csv" }, { "name" : "custom", "type" : "object", - "ref" : "custom", - "required" : true + "ref" : "custom" }, { "name" : "dfdl", "type" : "object", - "ref" : "dfdl", - "required" : true + "ref" : "dfdl" }, { "name" : "fhirJson", "type" : "object", - "ref" : "fhirJson", - "required" : true + "ref" : "fhirJson" }, { "name" : "fhirXml", "type" : "object", - "ref" : "fhirXml", - "required" : true + "ref" : "fhirXml" }, { "name" : "flatpack", "type" : "object", - "ref" : "flatpack", - "required" : true + "ref" : "flatpack" }, { "name" : "fory", "type" : "object", - "ref" : "fory", - "required" : true + "ref" : "fory" }, { "name" : "grok", "type" : "object", - "ref" : "grok", - "required" : true + "ref" : "grok" }, { "name" : "groovyJson", "type" : "object", - "ref" : "groovyJSon", - "required" : true + "ref" : "groovyJSon" }, { "name" : "groovyXml", "type" : "object", - "ref" : "groovyXml", - "required" : true + "ref" : "groovyXml" }, { "name" : "gzipDeflater", "type" : "object", - "ref" : "gzipDeflater", - "required" : true + "ref" : "gzipDeflater" }, { "name" : "hl7", "type" : "object", - "ref" : "hL7", - "required" : true + "ref" : "hL7" }, { "name" : "ical", "type" : "object", - "ref" : "ical", - "required" : true + "ref" : "ical" }, { "name" : "iso8583", "type" : "object", - "ref" : "iso8583", - "required" : true + "ref" : "iso8583" }, { "name" : "jacksonXml", "type" : "object", - "ref" : "jacksonXML", - "required" : true + "ref" : "jacksonXML" }, { "name" : "jaxb", "type" : "object", - "ref" : "jaxb", - "required" : true + "ref" : "jaxb" }, { "name" : "json", "type" : "object", - "ref" : "json", - "required" : true + "ref" : "json" }, { "name" : "jsonApi", "type" : "object", - "ref" : "jsonApi", - "required" : true + "ref" : "jsonApi" }, { "name" : "lzf", "type" : "object", - "ref" : "lZF", - "required" : true + "ref" : "lZF" }, { "name" : "mimeMultipart", "type" : "object", - "ref" : "mimeMultipart", - "required" : true + "ref" : "mimeMultipart" }, { "name" : "ocsf", "type" : "object", - "ref" : "ocsf", - "required" : true + "ref" : "ocsf" }, { "name" : "parquetAvro", "type" : "object", - "ref" : "parquetAvro", - "required" : true + "ref" : "parquetAvro" }, { "name" : "pgp", "type" : "object", - "ref" : "pGP", - "required" : true + "ref" : "pGP" }, { "name" : "pqc", "type" : "object", - "ref" : "pQC", - "required" : true + "ref" : "pQC" }, { "name" : "protobuf", "type" : "object", - "ref" : "protobuf", - "required" : true + "ref" : "protobuf" }, { "name" : "rss", "type" : "object", - "ref" : "rss", - "required" : true + "ref" : "rss" }, { "name" : "smooks", "type" : "object", - "ref" : "smooks", - "required" : true + "ref" : "smooks" }, { "name" : "soap", "type" : "object", - "ref" : "soap", - "required" : true + "ref" : "soap" }, { "name" : "swiftMt", "type" : "object", - "ref" : "swiftMt", - "required" : true + "ref" : "swiftMt" }, { "name" : "swiftMx", "type" : "object", - "ref" : "swiftMx", - "required" : true + "ref" : "swiftMx" }, { "name" : "syslog", "type" : "object", - "ref" : "syslog", - "required" : true + "ref" : "syslog" }, { "name" : "tarFile", "type" : "object", - "ref" : "tarFile", - "required" : true + "ref" : "tarFile" }, { "name" : "thrift", "type" : "object", - "ref" : "thrift", - "required" : true + "ref" : "thrift" }, { "name" : "toon", "type" : "object", - "ref" : "toon", - "required" : true + "ref" : "toon" }, { "name" : "ubl", "type" : "object", - "ref" : "ubl", - "required" : true + "ref" : "ubl" }, { "name" : "univocityCsv", "type" : "object", - "ref" : "uniVocityCsv", - "required" : true + "ref" : "uniVocityCsv" }, { "name" : "univocityFixed", "type" : "object", - "ref" : "uniVocityFixed", - "required" : true + "ref" : "uniVocityFixed" }, { "name" : "univocityTsv", "type" : "object", - "ref" : "uniVocityTsv", - "required" : true + "ref" : "uniVocityTsv" }, { "name" : "xmlSecurity", "type" : "object", - "ref" : "xMLSecurity", - "required" : true + "ref" : "xMLSecurity" }, { "name" : "yaml", "type" : "object", - "ref" : "yAML", - "required" : true + "ref" : "yAML" }, { "name" : "zipDeflater", "type" : "object", - "ref" : "zipDeflater", - "required" : true + "ref" : "zipDeflater" }, { "name" : "zipFile", "type" : "object", - "ref" : "zipFile", - "required" : true + "ref" : "zipFile" } ] }, "validate" : { diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json index 42e25c573ed0..c54c9eaa9591 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json @@ -3168,110 +3168,6 @@ "$ref" : "#/items/definitions/org.apache.camel.model.dataformat.ASN1DataFormat" } } - }, { - "not" : { - "anyOf" : [ { - "required" : [ "asn1" ] - }, { - "required" : [ "avro" ] - }, { - "required" : [ "barcode" ] - }, { - "required" : [ "base64" ] - }, { - "required" : [ "beanio" ] - }, { - "required" : [ "bindy" ] - }, { - "required" : [ "cbor" ] - }, { - "required" : [ "crypto" ] - }, { - "required" : [ "csv" ] - }, { - "required" : [ "custom" ] - }, { - "required" : [ "dfdl" ] - }, { - "required" : [ "fhirJson" ] - }, { - "required" : [ "fhirXml" ] - }, { - "required" : [ "flatpack" ] - }, { - "required" : [ "fory" ] - }, { - "required" : [ "grok" ] - }, { - "required" : [ "groovyJson" ] - }, { - "required" : [ "groovyXml" ] - }, { - "required" : [ "gzipDeflater" ] - }, { - "required" : [ "hl7" ] - }, { - "required" : [ "ical" ] - }, { - "required" : [ "iso8583" ] - }, { - "required" : [ "jacksonXml" ] - }, { - "required" : [ "jaxb" ] - }, { - "required" : [ "json" ] - }, { - "required" : [ "jsonApi" ] - }, { - "required" : [ "lzf" ] - }, { - "required" : [ "mimeMultipart" ] - }, { - "required" : [ "ocsf" ] - }, { - "required" : [ "parquetAvro" ] - }, { - "required" : [ "pgp" ] - }, { - "required" : [ "pqc" ] - }, { - "required" : [ "protobuf" ] - }, { - "required" : [ "rss" ] - }, { - "required" : [ "smooks" ] - }, { - "required" : [ "soap" ] - }, { - "required" : [ "swiftMt" ] - }, { - "required" : [ "swiftMx" ] - }, { - "required" : [ "syslog" ] - }, { - "required" : [ "tarFile" ] - }, { - "required" : [ "thrift" ] - }, { - "required" : [ "toon" ] - }, { - "required" : [ "ubl" ] - }, { - "required" : [ "univocityCsv" ] - }, { - "required" : [ "univocityFixed" ] - }, { - "required" : [ "univocityTsv" ] - }, { - "required" : [ "xmlSecurity" ] - }, { - "required" : [ "yaml" ] - }, { - "required" : [ "zipDeflater" ] - }, { - "required" : [ "zipFile" ] - } ] - } }, { "type" : "object", "required" : [ "avro" ], diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/MarshalTest.groovy b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/MarshalTest.groovy index 7ce4877cb22b..18b80c7b0bdb 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/MarshalTest.groovy +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/MarshalTest.groovy @@ -108,17 +108,21 @@ class MarshalTest extends YamlTestSupport { } } - def "no dataformat"() { + def "Error: no dataformat"() { when: var route = ''' - from: uri: "direct:start" - steps: + steps: - marshal: {} - to: "mock:result" ''' - loadRoutes(route) then: - context.routeDefinitions.size() == 1 + try { + loadRoutes(route) + Assertions.fail("Should have thrown exception") + } catch (IllegalArgumentException e) { + Assertions.assertTrue(e.getMessage().contains("0 are valid"), e.getMessage()) + } } }
