This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch feature/CAMEL-24711-validate-camel-version in repository https://gitbox.apache.org/repos/asf/camel.git
commit 7df12d9214faf0c401e3ceb95f02b6c675ed6da8 Author: Claus Ibsen <[email protected]> AuthorDate: Wed Sep 16 07:55:41 2026 +0200 CAMEL-24711: camel-jbang - camel validate yaml and camel validate source take --camel-version and --runtime: the catalog of that version and runtime, and the YAML DSL schema read from the camel-yaml-dsl jar of that version; a component without a Quarkus extension or Spring Boot starter is an error; camel_validate_source validates against the schema of camelVersion Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../modules/ROOT/pages/camel-jbang-devtools.adoc | 34 +++++++ .../modules/ROOT/pages/camel-jbang-mcp.adoc | 4 +- .../dsl/jbang/core/commands/ai/EndpointChecks.java | 41 +++++++- .../jbang/core/commands/ai/SourceValidator.java | 67 ++++++++++++- .../camel/dsl/jbang/core/common/CatalogLoader.java | 48 +++++++++ .../commands/ai/SourceValidatorVersionTest.java | 107 +++++++++++++++++++++ .../commands/ai/fakequarkus/components/log.json | 68 +++++++++++++ .../commands/ai/fakequarkus/components/timer.json | 52 ++++++++++ .../camel-jbang-plugin-validate/pom.xml | 12 +++ .../commands/validate/CatalogVersionMixin.java | 98 +++++++++++++++++++ .../commands/validate/SourceValidateCommand.java | 12 ++- .../commands/validate/YamlValidateCommand.java | 11 ++- .../validate/ValidateCamelVersionTest.java | 100 +++++++++++++++++++ .../src/test/resources/no-extension.yaml | 21 ++++ .../src/test/resources/route.yaml | 28 ++++++ .../camel/dsl/yaml/validator/YamlValidator.java | 24 ++++- .../validator/YamlValidatorSchemaDocumentTest.java | 59 ++++++++++++ 17 files changed, 771 insertions(+), 15 deletions(-) diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-devtools.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang-devtools.adoc index d8f1bda0b31c..574f18cfc282 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-jbang-devtools.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-devtools.adoc @@ -421,3 +421,37 @@ When all files pass: $ camel validate yaml cheese.yaml bad.yaml Validation success (files:2) ---- + +`camel validate yaml` checks the YAML DSL schema, then the endpoint URIs, Simple expressions and bean references +against the Camel catalog (`--catalog=false` for the schema only); `--canonical` validates against the canonical +schema and reports the deprecated compact notation. `camel validate source` validates any file `camel run` would +load: YAML routes, `.properties` files (the `camel.*` keys), Java (compiles it), XSLT and XML. + +=== Validating against another Camel version or runtime + +Both commands validate against the CLI's own Camel version. A project that runs another version, or Camel +Spring Boot or Camel Quarkus, is validated against the catalog and the YAML DSL schema of that version with +`--camel-version` and `--runtime`, the same options `camel catalog` and `camel doc` take: + +[source,bash] +---- +$ camel validate yaml --camel-version=4.18.0 orders.yaml +$ camel validate source --runtime=quarkus --quarkus-version=3.30.1 orders.yaml application.properties +Validation error detected (errors:1) + + File: orders.yaml + line 4: atmosphere-websocket: Camel Quarkus has no extension for this component (no camel-quarkus-atmosphere-websocket); pick a component that has one, camel_catalog_find lists them +---- + +The catalog of that version is downloaded as for `camel catalog`, and the schema is read from the +`camel-yaml-dsl` jar of that version (`--repos` and `--download=false` apply to both). With `--runtime=quarkus` +the Quarkus platform is resolved from the Camel version through the Quarkus extension registry (cached for a +day under `~/.camel`), or given with `--quarkus-version`; a component of the route that has no Camel Quarkus +extension, or no Camel Spring Boot starter with `--runtime=spring-boot`, is an error. `--canonical` needs Camel +4.22 or later, the first version with the canonical schema. + +The Simple expressions are parsed by the CLI's own Simple language (the catalog of the other version supplies +its functions), and the Java, XSLT and XML checks of `camel validate source` use the CLI's classpath. + +The `camel_validate_source` tool of the MCP servers (xref:camel-jbang-mcp.adoc[]) does the same when it is +given a `camelVersion`: the catalog and the schema of that version. diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc index 3a53d0cacf74..6b272ccadbd8 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc @@ -310,7 +310,9 @@ project `directory` as an argument, the runtime tools take the integration `name | `camel_validate_source` | Validates Camel YAML DSL or `.properties` source without writing: the YAML DSL schema (a misspelled option such as `logLevel` instead of `loggingLevel`), endpoint URIs, simple expressions, and `camel.*` options. - Takes the content, or reads the file from the project directory. + Takes the content, or reads the file from the project directory. With `camelVersion` (or the version of the + selected integration in the TUI) the catalog and the YAML DSL schema of that Camel version answer, the schema + read from its `camel-yaml-dsl` jar; without it the CLI's own, with nothing to download. | `camel_get_files` | The source files of a project directory (name, size, type), or the content of one of them. diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java index f992dd13de86..2ffe970314d1 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java @@ -26,7 +26,9 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.camel.catalog.CamelCatalog; +import org.apache.camel.catalog.DefaultCamelCatalog; import org.apache.camel.catalog.EndpointValidationResult; +import org.apache.camel.catalog.RuntimeProvider; import static org.apache.camel.dsl.jbang.core.commands.ai.YamlLines.YAML_URI_PATTERN; import static org.apache.camel.dsl.jbang.core.commands.ai.YamlLines.countLeadingSpaces; @@ -179,8 +181,15 @@ final class EndpointChecks { try { EndpointValidationResult result = catalog.validateEndpointProperties(fullUri, false, consumerOnly, producerOnly); + String scheme = fullUri.contains(":") ? fullUri.substring(0, fullUri.indexOf(':')) : fullUri; + if (result.getUnknownComponent() != null) { + // a warning to the catalog, an error when the runtime is what lacks the component + String missing = missingInRuntime(catalog, scheme); + if (missing != null) { + errors.add(linePrefix(i) + missing); + } + } if (!result.isSuccess()) { - String scheme = fullUri.contains(":") ? fullUri.substring(0, fullUri.indexOf(':')) : fullUri; collectEndpointErrors(errors, result, scheme, i, optionLineMap); } checkRegexOptions(errors, fullUri, i, optionLineMap); @@ -191,6 +200,36 @@ final class EndpointChecks { return errors; } + private static volatile CamelCatalog defaultCatalog; + + /** + * The message for a component the catalog of a runtime (Camel Quarkus, Camel Spring Boot) does not have while Camel + * has it: the runtime has no extension or starter for it. Null for the default catalog, whose unknown components + * are not reported: a project can register a component of its own (CAMEL-24711). + */ + static String missingInRuntime(CamelCatalog catalog, String scheme) { + RuntimeProvider provider = catalog.getRuntimeProvider(); + String name = provider != null ? provider.getProviderName() : null; + if (name == null || "default".equals(name)) { + return null; + } + CamelCatalog plain = defaultCatalog; + if (plain == null) { + plain = new DefaultCamelCatalog(); + defaultCatalog = plain; + } + if (plain.componentModel(scheme) == null) { + return null; + } + return switch (name) { + case "quarkus" -> scheme + ": Camel Quarkus has no extension for this component (no camel-quarkus-" + scheme + + "); pick a component that has one, camel_catalog_find lists them"; + case "springboot" -> scheme + ": Camel Spring Boot has no starter for this component (no camel-" + scheme + + "-starter)"; + default -> scheme + ": the " + name + " runtime has no support for this component"; + }; + } + /** Options models write that the component does not have, and what the component does instead. */ static final Map<String, String> INVENTED_OPTIONS = Map.ofEntries( Map.entry("file:mkdir", "directories are created by default (autoCreate=true); remove the option"), diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java index 463e416d2284..939c3672a0a1 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java @@ -26,12 +26,15 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.networknt.schema.Error; import org.apache.camel.catalog.CamelCatalog; +import org.apache.camel.catalog.DefaultCamelCatalog; +import org.apache.camel.dsl.jbang.core.common.CatalogLoader; import org.apache.camel.dsl.yaml.validator.YamlValidator; import static org.apache.camel.dsl.jbang.core.commands.ai.JavaChecks.JAVA_CLASS_PATTERN; @@ -47,7 +50,10 @@ import static org.apache.camel.dsl.jbang.core.commands.ai.JavaChecks.withSibling */ public final class SourceValidator { + private static final String BUILTIN_VERSION = new DefaultCamelCatalog().getCatalogVersion(); private static volatile YamlValidator yamlValidator; + /** The schema validators of other Camel versions, by version (CAMEL-24711). */ + private static final Map<String, YamlValidator> VERSION_VALIDATORS = new ConcurrentHashMap<>(); private SourceValidator() { } @@ -83,9 +89,19 @@ public final class SourceValidator { public static List<String> validate( String fileName, String content, CamelCatalog catalog, Function<String, String> extraPropertyLine, Path directory) { + return validate(fileName, content, catalog, extraPropertyLine, directory, null); + } + + /** + * As {@link #validate(String, String, CamelCatalog, Function, Path)}, with the YAML DSL schema validator to use: + * one built for the schema of another Camel version, or null for the schema of the catalog's version. + */ + public static List<String> validate( + String fileName, String content, CamelCatalog catalog, Function<String, String> extraPropertyLine, + Path directory, YamlValidator schemaValidator) { String name = fileName == null ? "" : fileName.toLowerCase(Locale.ROOT); if (name.endsWith(".yaml") || name.endsWith(".yml")) { - List<String> msgs = validateCamelYaml(content, catalog); + List<String> msgs = validateCamelYaml(content, catalog, schemaValidator); if (directory != null && msgs.isEmpty()) { msgs = new ArrayList<>(msgs); msgs.addAll(validateYamlBeanRefs(content, BeanDeclarations.scan(directory, fileName), catalog)); @@ -114,15 +130,33 @@ public final class SourceValidator { /** * Validates Camel YAML DSL source: the YAML DSL schema first, then endpoint URIs and simple expressions against the - * catalog. Returns the messages, empty when the source is valid. + * catalog. The schema is the one of the catalog's Camel version: the CLI's own, or for a catalog of another version + * the schema read from the {@code camel-yaml-dsl} jar of that version. Returns the messages, empty when the source + * is valid. */ public static List<String> validateCamelYaml(String content, CamelCatalog catalog) { + return validateCamelYaml(content, catalog, null); + } + + /** + * As {@link #validateCamelYaml(String, CamelCatalog)} with the schema validator to use, null for the one of the + * catalog's version. + */ + public static List<String> validateCamelYaml(String content, CamelCatalog catalog, YamlValidator schemaValidator) { List<String> msgs = new ArrayList<>(); if (content == null || content.isBlank()) { return msgs; } + YamlValidator validator; + try { + validator = schemaValidator != null ? schemaValidator : yamlValidator(catalog); + } catch (Exception e) { + msgs.add("Cannot validate against the YAML DSL schema of Camel " + catalog.getCatalogVersion() + ": " + + e.getMessage()); + return msgs; + } try { - msgs.addAll(formatSchemaErrors(yamlValidator().validate(content))); + msgs.addAll(formatSchemaErrors(validator.validate(content))); } catch (Exception e) { msgs.add("Invalid YAML: " + e.getMessage()); return msgs; @@ -131,6 +165,33 @@ public final class SourceValidator { return msgs; } + /** + * The schema validator of the catalog's Camel version: the CLI's own when the catalog is the built-in one, else one + * for the schema of the version the catalog was loaded for (CAMEL-24711), built once per version. + */ + static YamlValidator yamlValidator(CamelCatalog catalog) throws Exception { + String version = catalog != null ? catalog.getCatalogVersion() : null; + if (version == null || version.equals(BUILTIN_VERSION)) { + return yamlValidator(); + } + YamlValidator v = VERSION_VALIDATORS.get(version); + if (v == null) { + synchronized (VERSION_VALIDATORS) { + v = VERSION_VALIDATORS.get(version); + if (v == null) { + String schema = CatalogLoader.loadYamlDslSchema(null, version, false, true); + if (schema == null) { + return yamlValidator(); + } + v = new YamlValidator(false, schema, catalog); + v.init(); + VERSION_VALIDATORS.put(version, v); + } + } + } + return v; + } + /** * The catalog checks of a YAML route without the schema: endpoint URIs and Simple expressions. For a caller that * has already validated the schema (the CLI, the schema tool). diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CatalogLoader.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CatalogLoader.java index e581eb20479d..b17c61d887a7 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CatalogLoader.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CatalogLoader.java @@ -19,8 +19,11 @@ package org.apache.camel.dsl.jbang.core.common; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.function.Function; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; @@ -240,6 +243,51 @@ public final class CatalogLoader { return answer; } + /** + * The YAML DSL JSON schema of a Camel version, read from the {@code org.apache.camel:camel-yaml-dsl} jar of that + * version (every 4.x release ships {@code schema/camelYamlDsl.json}; the canonical schema exists from 4.22). + * + * @param repos extra Maven repositories, comma separated; null for the defaults + * @param version the Camel version + * @param canonical whether to read the canonical schema + * @param download whether to download when the jar is not in the local repository + * @return the schema document as JSON, or null when the version is the one of the CLI, whose schema is + * on the classpath + * @throws IOException when the jar cannot be downloaded or has no such schema + */ + public static String loadYamlDslSchema(String repos, String version, boolean canonical, boolean download) + throws Exception { + if (version == null || version.isBlank() || version.equals(new DefaultCamelCatalog().getCatalogVersion())) { + return null; + } + String entry = canonical ? "schema/camelYamlDsl-canonical.json" : "schema/camelYamlDsl.json"; + MavenDependencyDownloader downloader = new MavenDependencyDownloader(); + downloader.setRepositories(repos); + downloader.setDownload(download); + try { + downloader.start(); + MavenArtifact ma = downloader.downloadArtifact("org.apache.camel", "camel-yaml-dsl", version); + if (ma == null || ma.getFile() == null) { + throw new IOException("Cannot download org.apache.camel:camel-yaml-dsl:" + version); + } + try (ZipFile jar = new ZipFile(ma.getFile())) { + ZipEntry ze = jar.getEntry(entry); + if (ze == null) { + throw new IOException( + canonical + ? "Camel " + version + " has no canonical YAML DSL schema (it exists from Camel 4.22)" + : "org.apache.camel:camel-yaml-dsl:" + version + " has no " + entry); + } + try (InputStream is = jar.getInputStream(ze)) { + return new String(is.readAllBytes(), StandardCharsets.UTF_8); + } + } + } finally { + downloader.stop(); + downloader.close(); + } + } + public static String resolveCamelVersionFromSpringBoot(String repos, String camelSpringBootVersion, boolean download) throws Exception { DependencyDownloaderClassLoader cl = new DependencyDownloaderClassLoader(CatalogLoader.class.getClassLoader()); diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorVersionTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorVersionTest.java new file mode 100644 index 000000000000..b105e6e0c8e4 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorVersionTest.java @@ -0,0 +1,107 @@ +/* + * 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. + */ +package org.apache.camel.dsl.jbang.core.commands.ai; + +import java.util.List; + +import org.apache.camel.catalog.CamelCatalog; +import org.apache.camel.catalog.DefaultCamelCatalog; +import org.apache.camel.catalog.DefaultRuntimeProvider; +import org.apache.camel.dsl.jbang.core.common.CatalogLoader; +import org.apache.camel.dsl.yaml.validator.YamlValidator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIfSystemProperty; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * The validation of a source for another Camel version or runtime than the CLI's own (CAMEL-24711): the schema of that + * version, and a component the runtime has no extension for. + */ +class SourceValidatorVersionTest { + + private static final String ROUTE = """ + - from: + uri: timer:tick + steps: + - to: kafka:orders + - to: log:done + """; + + /** A catalog that knows timer and log only, the way the camel-quarkus-catalog knows the extensions. */ + static final class FakeQuarkusProvider extends DefaultRuntimeProvider { + @Override + public String getProviderName() { + return "quarkus"; + } + + @Override + public String getComponentJSonSchemaDirectory() { + return "org/apache/camel/dsl/jbang/core/commands/ai/fakequarkus/components"; + } + } + + @Test + void aComponentWithoutAnExtensionIsAnErrorOnTheRuntimeCatalog() { + CamelCatalog quarkus = new DefaultCamelCatalog(); + quarkus.setRuntimeProvider(new FakeQuarkusProvider()); + + List<String> msgs = SourceValidator.validateYamlEndpoints(ROUTE, quarkus); + assertThat(msgs).hasSize(1); + assertThat(msgs.get(0)).contains("kafka: Camel Quarkus has no extension").contains("camel-quarkus-kafka"); + + // the plain catalog says nothing about a scheme it does not know: a project can register a component of its own + assertThat(SourceValidator.validateYamlEndpoints(ROUTE.replace("kafka:orders", "mine:orders"), + new DefaultCamelCatalog())).isEmpty(); + // and neither does the runtime catalog, only Camel components without an extension are reported + assertThat(SourceValidator.validateYamlEndpoints(ROUTE.replace("kafka:orders", "mine:orders"), quarkus)).isEmpty(); + } + + @Test + void theBuiltInCatalogUsesTheBuiltInSchema() throws Exception { + YamlValidator v = SourceValidator.yamlValidator(new DefaultCamelCatalog()); + assertThat(v).isSameAs(SourceValidator.yamlValidator(null)); + } + + @Test + @DisabledIfSystemProperty(named = "ci.env.name", matches = ".*", + disabledReason = "Runs only local — requires the camel-yaml-dsl jar of another version") + void aCatalogOfAnotherVersionGetsTheSchemaOfThatVersion() throws Exception { + CamelCatalog older = CatalogLoader.loadCatalog(null, "4.18.0", true); + assertThat(older.getCatalogVersion()).isEqualTo("4.18.0"); + + YamlValidator v = SourceValidator.yamlValidator(older); + assertThat(v).isNotSameAs(SourceValidator.yamlValidator(null)); + assertThat(SourceValidator.yamlValidator(older)).as("one validator per version").isSameAs(v); + + // a route both versions accept validates; the schema read from the 4.18.0 jar is the one that answers + assertThat(SourceValidator.validateCamelYaml(ROUTE, older)).isEmpty(); + assertThat(CatalogLoader.loadYamlDslSchema(null, "4.18.0", false, true)).contains("\"$schema\""); + // the CLI's own version has its schema on the classpath: nothing to read + assertThat(CatalogLoader.loadYamlDslSchema(null, new DefaultCamelCatalog().getCatalogVersion(), false, true)) + .isNull(); + } + + @Test + @DisabledIfSystemProperty(named = "ci.env.name", matches = ".*", + disabledReason = "Runs only local — requires the camel-yaml-dsl jar of another version") + void theCanonicalSchemaExistsFromCamel422() { + org.assertj.core.api.Assertions.assertThatThrownBy(() -> CatalogLoader.loadYamlDslSchema(null, "4.18.0", true, true)) + .hasMessageContaining("no canonical YAML DSL schema") + .hasMessageContaining("4.22"); + } +} diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/resources/org/apache/camel/dsl/jbang/core/commands/ai/fakequarkus/components/log.json b/dsl/camel-jbang/camel-jbang-core/src/test/resources/org/apache/camel/dsl/jbang/core/commands/ai/fakequarkus/components/log.json new file mode 100644 index 000000000000..553eac237c7d --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/test/resources/org/apache/camel/dsl/jbang/core/commands/ai/fakequarkus/components/log.json @@ -0,0 +1,68 @@ +{ + "component": { + "kind": "component", + "name": "log", + "title": "Log Data", + "description": "Prints data from the routed message (such as body and headers) to the logger.", + "deprecated": false, + "firstVersion": "1.1.0", + "label": "core,monitoring", + "javaType": "org.apache.camel.component.log.LogComponent", + "supportLevel": "Stable", + "groupId": "org.apache.camel", + "artifactId": "camel-log", + "version": "4.23.0-SNAPSHOT", + "scheme": "log", + "extendsScheme": "", + "syntax": "log:loggerName", + "async": false, + "api": false, + "consumerOnly": false, + "producerOnly": true, + "lenientProperties": false, + "browsable": false, + "remote": false + }, + "componentProperties": { + "lazyStartProducer": { "index": 0, "kind": "property", "displayName": "Lazy Start Producer", "group": "producer", "label": "producer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail [...] + "sourceLocationLoggerName": { "index": 1, "kind": "property", "displayName": "Source Location Logger Name", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "If enabled then the source location of where the log endpoint is used in Camel routes, would be used as logger name, instead of the given name. However, if the source location is disabled [...] + "autowiredEnabled": { "index": 2, "kind": "property", "displayName": "Autowired Enabled", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Whether autowiring is enabled. This is used for automatic autowiring options (the option must be marked as autowired) by looking up in the registry to find if there is a single instance of matching t [...] + "exchangeFormatter": { "index": 3, "kind": "property", "displayName": "Exchange Formatter", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.ExchangeFormatter", "deprecated": false, "autowired": true, "secret": false, "description": "Sets a custom ExchangeFormatter to convert the Exchange to a String suitable for logging. If not specified, we default to DefaultExchangeFormatter." } + }, + "properties": { + "loggerName": { "index": 0, "kind": "path", "displayName": "Logger Name", "group": "producer", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Name of the logging category to use" }, + "groupActiveOnly": { "index": 1, "kind": "parameter", "displayName": "Group Active Only", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "If true, will hide stats when no new messages have been received for a time interval, if false, show stats regardless of message traffic." }, + "groupDelay": { "index": 2, "kind": "parameter", "displayName": "Group Delay", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "description": "Set the initial delay for stats (in millis)" }, + "groupInterval": { "index": 3, "kind": "parameter", "displayName": "Group Interval", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "description": "If specified will group message stats by this time interval (in millis)" }, + "groupSize": { "index": 4, "kind": "parameter", "displayName": "Group Size", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "java.lang.Integer", "deprecated": false, "autowired": false, "secret": false, "description": "An integer that specifies a group size for throughput logging." }, + "level": { "index": 5, "kind": "parameter", "displayName": "Level", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "java.lang.String", "enum": [ "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "OFF" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "INFO", "description": "Logging level to use. The default value is INFO." }, + "logMask": { "index": 6, "kind": "parameter", "displayName": "Log Mask", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "If true, mask sensitive information like password or passphrase in the log." }, + "marker": { "index": 7, "kind": "parameter", "displayName": "Marker", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "An optional Marker name to use." }, + "plain": { "index": 8, "kind": "parameter", "displayName": "Plain", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "If enabled only the body will be printed out" }, + "sourceLocationLoggerName": { "index": 9, "kind": "parameter", "displayName": "Source Location Logger Name", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "If enabled then the source location of where the log endpoint is used in Camel routes, would be used as logger name, instead of the given name. However, if the source location is disabled [...] + "lazyStartProducer": { "index": 10, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a produ [...] + "exchangeFormatter": { "index": 11, "kind": "parameter", "displayName": "Exchange Formatter", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.ExchangeFormatter", "deprecated": false, "autowired": false, "secret": false, "description": "To use a custom exchange formatter" }, + "maxChars": { "index": 12, "kind": "parameter", "displayName": "Max Chars", "group": "formatting", "label": "formatting", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 10000, "description": "Limits the number of characters logged per line." }, + "multiline": { "index": 13, "kind": "parameter", "displayName": "Multiline", "group": "formatting", "label": "formatting", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "If enabled then each information is outputted on a newline." }, + "showAll": { "index": 14, "kind": "parameter", "displayName": "Show All", "group": "formatting", "label": "formatting", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Quick option for turning all options on. (multiline, maxChars has to be manually set if to be used)" }, + "showAllProperties": { "index": 15, "kind": "parameter", "displayName": "Show All Properties", "group": "formatting", "label": "formatting", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Show all of the exchange properties (both internal and custom)." }, + "showBody": { "index": 16, "kind": "parameter", "displayName": "Show Body", "group": "formatting", "label": "formatting", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Show the message body." }, + "showBodyType": { "index": 17, "kind": "parameter", "displayName": "Show Body Type", "group": "formatting", "label": "formatting", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Show the body Java type." }, + "showCachedStreams": { "index": 18, "kind": "parameter", "displayName": "Show Cached Streams", "group": "formatting", "label": "formatting", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Whether Camel should show cached stream bodies or not (org.apache.camel.StreamCache)." }, + "showCaughtException": { "index": 19, "kind": "parameter", "displayName": "Show Caught Exception", "group": "formatting", "label": "formatting", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "If the exchange has a caught exception, show the exception message (no stack trace). A caught exception is stored as a property on the exchange (using the key org.apache.camel.Exchange# [...] + "showException": { "index": 20, "kind": "parameter", "displayName": "Show Exception", "group": "formatting", "label": "formatting", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "If the exchange has an exception, show the exception message (no stacktrace)" }, + "showExchangeId": { "index": 21, "kind": "parameter", "displayName": "Show Exchange Id", "group": "formatting", "label": "formatting", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Show the unique exchange ID." }, + "showExchangePattern": { "index": 22, "kind": "parameter", "displayName": "Show Exchange Pattern", "group": "formatting", "label": "formatting", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Shows the Message Exchange Pattern (or MEP for short)." }, + "showFiles": { "index": 23, "kind": "parameter", "displayName": "Show Files", "group": "formatting", "label": "formatting", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "If enabled Camel will output files" }, + "showFuture": { "index": 24, "kind": "parameter", "displayName": "Show Future", "group": "formatting", "label": "formatting", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "If enabled Camel will on Future objects wait for it to complete to obtain the payload to be logged." }, + "showHeaders": { "index": 25, "kind": "parameter", "displayName": "Show Headers", "group": "formatting", "label": "formatting", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Show the message headers." }, + "showProperties": { "index": 26, "kind": "parameter", "displayName": "Show Properties", "group": "formatting", "label": "formatting", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Show the exchange properties (only custom). Use showAllProperties to show both internal and custom properties." }, + "showRouteGroup": { "index": 27, "kind": "parameter", "displayName": "Show Route Group", "group": "formatting", "label": "formatting", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Show route Group." }, + "showRouteId": { "index": 28, "kind": "parameter", "displayName": "Show Route Id", "group": "formatting", "label": "formatting", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Show route ID." }, + "showStackTrace": { "index": 29, "kind": "parameter", "displayName": "Show Stack Trace", "group": "formatting", "label": "formatting", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Show the stack trace, if an exchange has an exception. Only effective if one of showAll, showException or showCaughtException are enabled." }, + "showStreams": { "index": 30, "kind": "parameter", "displayName": "Show Streams", "group": "formatting", "label": "formatting", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether Camel should show stream bodies or not (eg such as java.io.InputStream). Beware if you enable this option then you may not be able later to access the message body as the stream have already bee [...] + "showVariables": { "index": 31, "kind": "parameter", "displayName": "Show Variables", "group": "formatting", "label": "formatting", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Show the variables." }, + "skipBodyLineSeparator": { "index": 32, "kind": "parameter", "displayName": "Skip Body Line Separator", "group": "formatting", "label": "formatting", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Whether to skip line separators when logging the message body. This allows to log the message body in one line, setting this option to false will preserve any line separators from t [...] + "style": { "index": 33, "kind": "parameter", "displayName": "Style", "group": "formatting", "label": "formatting", "required": false, "type": "enum", "javaType": "org.apache.camel.support.processor.DefaultExchangeFormatter.OutputStyle", "enum": [ "Default", "Tab", "Fixed" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "Default", "description": "Sets the outputs style to use." } + } +} diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/resources/org/apache/camel/dsl/jbang/core/commands/ai/fakequarkus/components/timer.json b/dsl/camel-jbang/camel-jbang-core/src/test/resources/org/apache/camel/dsl/jbang/core/commands/ai/fakequarkus/components/timer.json new file mode 100644 index 000000000000..30d99544faab --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/test/resources/org/apache/camel/dsl/jbang/core/commands/ai/fakequarkus/components/timer.json @@ -0,0 +1,52 @@ +{ + "component": { + "kind": "component", + "name": "timer", + "title": "Timer", + "description": "Generate messages in specified intervals using java.util.Timer.", + "deprecated": false, + "firstVersion": "1.0.0", + "label": "core,scheduling", + "javaType": "org.apache.camel.component.timer.TimerComponent", + "supportLevel": "Stable", + "groupId": "org.apache.camel", + "artifactId": "camel-timer", + "version": "4.23.0-SNAPSHOT", + "scheme": "timer", + "extendsScheme": "", + "syntax": "timer:timerName", + "async": false, + "api": false, + "consumerOnly": true, + "producerOnly": false, + "lenientProperties": false, + "browsable": false, + "remote": false + }, + "componentProperties": { + "bridgeErrorHandler": { "index": 0, "kind": "property", "displayName": "Bridge Error Handler", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Allows for bridging the consumer to the Camel routing Error Handler, which mean any exceptions (if possible) occurred while the Camel consumer is trying to pickup incoming messages, or the like [...] + "includeMetadata": { "index": 1, "kind": "property", "displayName": "Include Metadata", "group": "consumer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether to include metadata in the exchange such as fired time, timer name, timer count etc." }, + "autowiredEnabled": { "index": 2, "kind": "property", "displayName": "Autowired Enabled", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Whether autowiring is enabled. This is used for automatic autowiring options (the option must be marked as autowired) by looking up in the registry to find if there is a single instance of matching t [...] + }, + "headers": { + "CamelTimerFiredTime": { "index": 0, "kind": "header", "displayName": "", "group": "consumer", "label": "", "required": false, "javaType": "Date", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The fired time", "constantName": "org.apache.camel.component.timer.TimerConstants#HEADER_FIRED_TIME" }, + "CamelMessageTimestamp": { "index": 1, "kind": "header", "displayName": "", "group": "consumer", "label": "", "required": false, "javaType": "long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The timestamp of the message", "constantName": "org.apache.camel.component.timer.TimerConstants#HEADER_MESSAGE_TIMESTAMP" } + }, + "properties": { + "timerName": { "index": 0, "kind": "path", "displayName": "Timer Name", "group": "consumer", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The name of the timer" }, + "delay": { "index": 1, "kind": "parameter", "displayName": "Delay", "group": "consumer", "label": "", "required": false, "type": "duration", "javaType": "long", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "1000", "description": "The number of milliseconds to wait before the first event is generated. Should not be used in conjunction with the time option. The default value is 1000." }, + "fixedRate": { "index": 2, "kind": "parameter", "displayName": "Fixed Rate", "group": "consumer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Events take place at approximately regular intervals, separated by the specified period." }, + "includeMetadata": { "index": 3, "kind": "parameter", "displayName": "Include Metadata", "group": "consumer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether to include metadata in the exchange such as fired time, timer name, timer count etc." }, + "period": { "index": 4, "kind": "parameter", "displayName": "Period", "group": "consumer", "label": "", "required": false, "type": "duration", "javaType": "long", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "1000", "description": "Generate periodic events every period. Must be zero or positive value. The default value is 1000." }, + "repeatCount": { "index": 5, "kind": "parameter", "displayName": "Repeat Count", "group": "consumer", "label": "", "required": false, "type": "integer", "javaType": "long", "deprecated": false, "autowired": false, "secret": false, "description": "Specifies a maximum limit for the number of fires. Therefore, if you set it to 1, the timer will only fire once. If you set it to 5, it will only fire five times. A value of zero or negative means fire forever." }, + "bridgeErrorHandler": { "index": 6, "kind": "parameter", "displayName": "Bridge Error Handler", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Allows for bridging the consumer to the Camel routing Error Handler, which mean any exceptions (if possible) occurred while the Camel consumer is trying to pickup incoming [...] + "exceptionHandler": { "index": 7, "kind": "parameter", "displayName": "Exception Handler", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.ExceptionHandler", "optionalPrefix": "consumer.", "deprecated": false, "autowired": false, "secret": false, "description": "To let the consumer use a custom ExceptionHandler. Notice if the option bridgeErrorHandler is enabled then this option is not in use. By def [...] + "exchangePattern": { "index": 8, "kind": "parameter", "displayName": "Exchange Pattern", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "enum", "javaType": "org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut" ], "deprecated": false, "autowired": false, "secret": false, "description": "Sets the exchange pattern when the consumer creates an exchange." }, + "daemon": { "index": 9, "kind": "parameter", "displayName": "Daemon", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Specifies whether the thread associated with the timer endpoint runs as a daemon. The default value is true." }, + "pattern": { "index": 10, "kind": "parameter", "displayName": "Pattern", "group": "advanced", "label": "advanced", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Allows you to specify a custom Date pattern to use for setting the time option using URI syntax." }, + "synchronous": { "index": 11, "kind": "parameter", "displayName": "Synchronous", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Sets whether synchronous processing should be strictly used" }, + "time": { "index": 12, "kind": "parameter", "displayName": "Time", "group": "advanced", "label": "advanced", "required": false, "type": "string", "javaType": "java.util.Date", "deprecated": false, "autowired": false, "secret": false, "description": "A java.util.Date the first event should be generated. If using the URI, the pattern expected is: yyyy-MM-dd HH:mm:ss or yyyy-MM-dd'T'HH:mm:ss." }, + "timer": { "index": 13, "kind": "parameter", "displayName": "Timer", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "java.util.Timer", "deprecated": false, "autowired": false, "secret": false, "description": "To use a custom Timer" }, + "runLoggingLevel": { "index": 14, "kind": "parameter", "displayName": "Run Logging Level", "group": "scheduler", "label": "consumer,scheduler", "required": false, "type": "enum", "javaType": "org.apache.camel.LoggingLevel", "enum": [ "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "OFF" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "TRACE", "description": "The consumer logs a start\/complete log line when it polls. This option allows you to configure the log [...] + } +} diff --git a/dsl/camel-jbang/camel-jbang-plugin-validate/pom.xml b/dsl/camel-jbang/camel-jbang-plugin-validate/pom.xml index 8a281548b22a..d108e6dfdf2b 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-validate/pom.xml +++ b/dsl/camel-jbang/camel-jbang-plugin-validate/pom.xml @@ -49,5 +49,17 @@ <groupId>org.apache.camel</groupId> <artifactId>camel-yaml-dsl-validator</artifactId> </dependency> + + <!-- test --> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-test-junit6</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.assertj</groupId> + <artifactId>assertj-core</artifactId> + <scope>test</scope> + </dependency> </dependencies> </project> diff --git a/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/CatalogVersionMixin.java b/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/CatalogVersionMixin.java new file mode 100644 index 000000000000..4336a9c46092 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/CatalogVersionMixin.java @@ -0,0 +1,98 @@ +/* + * 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. + */ +package org.apache.camel.dsl.jbang.core.commands.validate; + +import org.apache.camel.catalog.CamelCatalog; +import org.apache.camel.catalog.DefaultCamelCatalog; +import org.apache.camel.dsl.jbang.core.commands.MavenResolverMixin; +import org.apache.camel.dsl.jbang.core.commands.QuarkusPlatformMixin; +import org.apache.camel.dsl.jbang.core.common.CatalogLoader; +import org.apache.camel.dsl.jbang.core.common.QuarkusHelper; +import org.apache.camel.dsl.jbang.core.common.RuntimeCompletionCandidates; +import org.apache.camel.dsl.jbang.core.common.RuntimeType; +import org.apache.camel.dsl.jbang.core.common.RuntimeTypeConverter; +import org.apache.camel.dsl.yaml.validator.YamlValidator; +import picocli.CommandLine; + +/** + * The Camel version and runtime a validation answers for (CAMEL-24711): the catalog of that version and runtime, as + * {@code camel catalog} and {@code camel doc} load it, and the YAML DSL schema of that Camel version read from its + * {@code camel-yaml-dsl} jar. Without options the CLI's own version, with nothing to download. + */ +public class CatalogVersionMixin { + + @CommandLine.Option(names = { "--camel-version" }, + description = "Validate against another Camel version than the CLI's own: its catalog and YAML DSL schema") + String camelVersion; + + @CommandLine.Option(names = { "--runtime" }, + completionCandidates = RuntimeCompletionCandidates.class, + converter = RuntimeTypeConverter.class, + description = "Runtime (${COMPLETION-CANDIDATES}); spring-boot and quarkus use the catalog of that" + + " runtime, so a component without a starter or extension is an error") + RuntimeType runtime; + + @CommandLine.Mixin + MavenResolverMixin mavenResolver; + + @CommandLine.Mixin + QuarkusPlatformMixin quarkusPlatform; + + /** The catalog and the Camel version it answers for. */ + public record Loaded(CamelCatalog catalog, String camelVersion) { + } + + /** + * Loads the catalog of the version and runtime asked for; the built-in catalog when none is. + */ + public Loaded load() throws Exception { + if (RuntimeType.springBoot == runtime) { + String version = camelVersion != null ? camelVersion : new DefaultCamelCatalog().getCatalogVersion(); + return new Loaded( + CatalogLoader.loadSpringBootCatalog(mavenResolver.repos(), version, mavenResolver.download()), version); + } + if (RuntimeType.quarkus == runtime) { + QuarkusHelper.QuarkusPlatformBom bom = quarkusPlatform.resolve(camelVersion, + mavenResolver.downloader()::resolveArtifact, mavenResolver.download(), mavenResolver.fresh()); + CamelCatalog catalog + = CatalogLoader.loadQuarkusCatalog(bom.quarkusCamelBom(), mavenResolver.downloader()::resolveArtifact); + // the Camel version the platform pins is the one the schema is read for + String version = bom.camelVersion() != null ? bom.camelVersion() : camelVersion; + return new Loaded(catalog, version); + } + if (camelVersion == null || camelVersion.isBlank()) { + return new Loaded(new DefaultCamelCatalog(), null); + } + return new Loaded( + CatalogLoader.loadCatalog(mavenResolver.repos(), camelVersion, mavenResolver.download()), + camelVersion); + } + + /** + * The YAML DSL schema validator of the loaded Camel version: the CLI's own schema for the CLI's version, else the + * schema read from the {@code camel-yaml-dsl} jar of that version. + * + * @throws Exception when the jar cannot be downloaded, or the version has no canonical schema (before 4.22) + */ + public YamlValidator yamlValidator(Loaded loaded, boolean canonical) throws Exception { + String schema = CatalogLoader.loadYamlDslSchema(mavenResolver.repos(), loaded.camelVersion(), canonical, + mavenResolver.download()); + YamlValidator validator = new YamlValidator(canonical, schema, loaded.catalog()); + validator.init(); + return validator; + } +} diff --git a/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/SourceValidateCommand.java b/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/SourceValidateCommand.java index 71ec89464f83..b5fa97808a53 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/SourceValidateCommand.java +++ b/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/SourceValidateCommand.java @@ -25,10 +25,10 @@ import java.util.Map; import java.util.Stack; import org.apache.camel.catalog.CamelCatalog; -import org.apache.camel.catalog.DefaultCamelCatalog; import org.apache.camel.dsl.jbang.core.commands.CamelCommand; import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; import org.apache.camel.dsl.jbang.core.commands.ai.SourceValidator; +import org.apache.camel.dsl.yaml.validator.YamlValidator; import picocli.CommandLine; /** @@ -45,13 +45,19 @@ public class SourceValidateCommand extends CamelCommand { parameterConsumer = FilesConsumer.class) List<String> files = new ArrayList<>(); + @CommandLine.Mixin + CatalogVersionMixin catalogVersion; + public SourceValidateCommand(CamelJBangMain main) { super(main); } @Override public Integer doCall() throws Exception { - CamelCatalog catalog = new DefaultCamelCatalog(); + // the catalog and schema of the Camel version and runtime asked for; the CLI's own without options + CatalogVersionMixin.Loaded loaded = catalogVersion.load(); + CamelCatalog catalog = loaded.catalog(); + YamlValidator schemaValidator = loaded.camelVersion() != null ? catalogVersion.yamlValidator(loaded, false) : null; Map<String, List<String>> reports = new LinkedHashMap<>(); for (String n : files) { File f = new File(n); @@ -62,7 +68,7 @@ public class SourceValidateCommand extends CamelCommand { String content = Files.readString(f.toPath()); File parent = f.getAbsoluteFile().getParentFile(); reports.put(n, SourceValidator.validate(f.getName(), content, catalog, null, - parent != null ? parent.toPath() : null)); + parent != null ? parent.toPath() : null, schemaValidator)); } int count = reports.values().stream().mapToInt(List::size).sum(); if (count > 0) { diff --git a/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/YamlValidateCommand.java b/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/YamlValidateCommand.java index e0591e317f7a..68d8112311c9 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/YamlValidateCommand.java +++ b/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/YamlValidateCommand.java @@ -28,7 +28,6 @@ import java.util.Stack; import com.networknt.schema.Error; import org.apache.camel.catalog.CamelCatalog; -import org.apache.camel.catalog.DefaultCamelCatalog; import org.apache.camel.dsl.jbang.core.commands.CamelCommand; import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; import org.apache.camel.dsl.jbang.core.commands.ai.SourceValidator; @@ -51,6 +50,9 @@ public class YamlValidateCommand extends CamelCommand { + " (use --catalog=false for the schema only)") boolean catalog = true; + @CommandLine.Mixin + CatalogVersionMixin catalogVersion; + @CommandLine.Parameters(description = { "The Camel YAML source files to parse." }, arity = "1..9", paramLabel = "<files>", @@ -63,10 +65,11 @@ public class YamlValidateCommand extends CamelCommand { @Override public Integer doCall() throws Exception { - YamlValidator validator = new YamlValidator(canonical); - validator.init(); + // the catalog and schema of the Camel version and runtime asked for; the CLI's own without options + CatalogVersionMixin.Loaded loaded = catalogVersion.load(); + YamlValidator validator = catalogVersion.yamlValidator(loaded, canonical); - CamelCatalog camelCatalog = catalog ? new DefaultCamelCatalog() : null; + CamelCatalog camelCatalog = catalog ? loaded.catalog() : null; Map<String, List<Error>> reports = new LinkedHashMap<>(); for (String n : files) { if (matchFile(n)) { diff --git a/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/java/org/apache/camel/dsl/jbang/core/commands/validate/ValidateCamelVersionTest.java b/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/java/org/apache/camel/dsl/jbang/core/commands/validate/ValidateCamelVersionTest.java new file mode 100644 index 000000000000..1d6b60b4ffc7 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/java/org/apache/camel/dsl/jbang/core/commands/validate/ValidateCamelVersionTest.java @@ -0,0 +1,100 @@ +/* + * 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. + */ +package org.apache.camel.dsl.jbang.core.commands.validate; + +import java.nio.file.Path; + +import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; +import org.apache.camel.dsl.jbang.core.common.StringPrinter; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIfSystemProperty; +import picocli.CommandLine; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * camel validate yaml and camel validate source against the catalog and YAML DSL schema of another Camel version or + * runtime (CAMEL-24711). Without options the CLI's own version answers, offline. + */ +class ValidateCamelVersionTest { + + private static final String ROUTE = Path.of("src/test/resources/route.yaml").toString(); + private static final String NO_EXTENSION = Path.of("src/test/resources/no-extension.yaml").toString(); + + /** The file based Quarkus extension registry of the camel-jbang-core tests: no registry call. */ + private static String quarkusExtRegistry() { + return "--quarkus-ext-registry=" + + Path.of("../camel-jbang-core/src/test/resources/registry.quarkus.io").toAbsolutePath().normalize().toUri(); + } + + private static StringPrinter printer; + + private static int yaml(String... args) throws Exception { + printer = new StringPrinter(); + YamlValidateCommand cmd = new YamlValidateCommand(new CamelJBangMain().withPrinter(printer)); + CommandLine.populateCommand(cmd, args); + return cmd.doCall(); + } + + private static int source(String... args) throws Exception { + printer = new StringPrinter(); + SourceValidateCommand cmd = new SourceValidateCommand(new CamelJBangMain().withPrinter(printer)); + CommandLine.populateCommand(cmd, args); + return cmd.doCall(); + } + + @Test + void theOwnVersionValidatesOffline() throws Exception { + assertThat(yaml(ROUTE)).isZero(); + assertThat(printer.getOutput()).contains("Validation success"); + assertThat(source(ROUTE)).isZero(); + assertThat(printer.getOutput()).contains("Validation success"); + + // the plain catalog knows every component: a Camel component without an extension is not its business + assertThat(yaml(NO_EXTENSION)).isZero(); + } + + @Test + @DisabledIfSystemProperty(named = "ci.env.name", matches = ".*", + disabledReason = "Runs only local — downloads the catalog and camel-yaml-dsl jar of another version") + void anotherCamelVersionValidatesWithItsSchemaAndCatalog() throws Exception { + assertThat(yaml("--camel-version=4.18.0", ROUTE)).isZero(); + assertThat(printer.getOutput()).contains("Validation success"); + assertThat(source("--camel-version=4.18.0", ROUTE)).isZero(); + assertThat(printer.getOutput()).contains("Validation success"); + + // the canonical schema exists from 4.22: an older version is an error, not a silent fallback + assertThatThrownBy(() -> yaml("--canonical", "--camel-version=4.18.0", ROUTE)) + .hasMessageContaining("no canonical YAML DSL schema").hasMessageContaining("4.22"); + } + + @Test + @DisabledIfSystemProperty(named = "ci.env.name", matches = ".*", + disabledReason = "Runs only local — downloads the Quarkus platform BOM and catalog") + void quarkusReportsAComponentWithoutAnExtension() throws Exception { + // Quarkus platform 3.30.1 pins Camel 4.16.0: the catalog of camel-quarkus-catalog 3.30.0, the schema of 4.16.0 + assertThat(yaml("--runtime=quarkus", "--quarkus-version=3.30.1", quarkusExtRegistry(), ROUTE)).isZero(); + assertThat(source("--runtime=quarkus", "--quarkus-version=3.30.1", quarkusExtRegistry(), ROUTE)).isZero(); + + assertThat(yaml("--runtime=quarkus", "--quarkus-version=3.30.1", quarkusExtRegistry(), NO_EXTENSION)).isEqualTo(1); + assertThat(printer.getOutput()).contains("atmosphere-websocket: Camel Quarkus has no extension"); + assertThat(source("--runtime=quarkus", "--quarkus-version=3.30.1", quarkusExtRegistry(), NO_EXTENSION)) + .isEqualTo(1); + assertThat(printer.getOutput()).contains("atmosphere-websocket: Camel Quarkus has no extension"); + } +} diff --git a/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/resources/no-extension.yaml b/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/resources/no-extension.yaml new file mode 100644 index 000000000000..df6b67904d5b --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/resources/no-extension.yaml @@ -0,0 +1,21 @@ +# +# 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. +# + +- from: + uri: timer:tick + steps: + - to: atmosphere-websocket:/chat diff --git a/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/resources/route.yaml b/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/resources/route.yaml new file mode 100644 index 000000000000..a31e4a593053 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/resources/route.yaml @@ -0,0 +1,28 @@ +# +# 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. +# + +- from: + uri: timer:tick + parameters: + period: 1000 + steps: + - setBody: + expression: + simple: + expression: "Hello ${random(1,10)}" + - to: kafka:orders + - to: log:done 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 366604c23b5a..9a050d5ad4c0 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 @@ -73,6 +73,8 @@ public class YamlValidator { private final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); private final boolean canonical; + private final String schemaJson; + private final CamelCatalog catalog; private Schema schema; private Map<String, OneOfGroup> oneOfGroups; @@ -84,7 +86,22 @@ public class YamlValidator { } public YamlValidator(boolean canonical) { + this(canonical, null, null); + } + + /** + * A validator for a schema document other than the one on the classpath: the schema of another Camel version, read + * from the {@code camel-yaml-dsl} jar of that version. + * + * @param canonical whether the document is the canonical schema + * @param schemaJson the schema document as JSON; null for the schema on the classpath + * @param catalog the catalog of the same Camel version, for the checks the schema cannot express; null for the + * catalog on the classpath + */ + public YamlValidator(boolean canonical, String schemaJson, CamelCatalog catalog) { this.canonical = canonical; + this.schemaJson = schemaJson; + this.catalog = catalog; } public boolean isCanonical() { @@ -778,9 +795,9 @@ public class YamlValidator { * 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() { + private Map<String, OneOfGroup> loadOneOfGroups() { Map<String, OneOfGroup> groups = new HashMap<>(); - CamelCatalog catalog = new DefaultCamelCatalog(); + CamelCatalog catalog = this.catalog != null ? this.catalog : new DefaultCamelCatalog(); for (String name : catalog.findModelNames()) { EipModel model = catalog.eipModel(name); if (model == null) { @@ -1270,7 +1287,8 @@ public class YamlValidator { public void init() throws Exception { String location = canonical ? LOCATION_CANONICAL : LOCATION; - var model = mapper.readTree(YamlValidator.class.getResourceAsStream(location)); + var model = schemaJson != null + ? mapper.readTree(schemaJson) : mapper.readTree(YamlValidator.class.getResourceAsStream(location)); this.model = model; this.topLevelEntries = new LinkedHashSet<>(); model.at("/items/properties").fieldNames().forEachRemaining(topLevelEntries::add); diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorSchemaDocumentTest.java b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorSchemaDocumentTest.java new file mode 100644 index 000000000000..e0db8ddf47d9 --- /dev/null +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorSchemaDocumentTest.java @@ -0,0 +1,59 @@ +/* + * 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. + */ +package org.apache.camel.dsl.yaml.validator; + +import java.io.File; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * A validator built for a schema document handed to it, as camel validate --camel-version reads the schema of another + * Camel version from its camel-yaml-dsl jar (CAMEL-24711), validates against that document and not the classpath. + */ +public class YamlValidatorSchemaDocumentTest { + + private static String schema(String location) throws Exception { + try (var is = YamlValidator.class.getResourceAsStream(location)) { + return new String(is.readAllBytes(), StandardCharsets.UTF_8); + } + } + + @Test + public void theGivenDocumentIsTheSchema() throws Exception { + File shorthand = new File("src/test/resources/canonical-invalid-log-shorthand.yaml"); + + // the classpath schema accepts the log shorthand + YamlValidator classpath = new YamlValidator(false, null, null); + classpath.init(); + assertThat(classpath.validate(shorthand)).isEmpty(); + + // the same validator given the canonical document as its schema rejects it: the document is what counts + YamlValidator given = new YamlValidator(false, schema("/schema/camelYamlDsl-canonical.json"), null); + given.init(); + assertThat(given.validate(shorthand)).isNotEmpty(); + assertThat(given.isCanonical()).isFalse(); + + // and the classic document given explicitly validates as the classpath one does + YamlValidator same = new YamlValidator(false, schema("/schema/camelYamlDsl.json"), null); + same.init(); + assertThat(same.validate(shorthand)).isEmpty(); + assertThat(same.validate(new File("src/test/resources/canonical-valid.yaml"))).isEmpty(); + } +}
