This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch feature/CAMEL-23724-simple-language-grammar
in repository https://gitbox.apache.org/repos/asf/camel.git

commit c2234b0011554ba5cbc24f937cd52da7797a4ff6
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Jun 9 21:52:46 2026 +0200

    CAMEL-23724: Add operators section to Simple language catalog
    
    Extends the Simple language JSON catalog (simple.json) with a 
machine-readable
    operators section documenting all 32 operators with their kind, syntax,
    precedence, descriptions, and usage examples.
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../org/apache/camel/spi/annotations/Language.java |  13 ++
 .../org/apache/camel/language/simple/simple.json   |  34 +++
 .../camel/language/simple/SimpleLanguage.java      |   2 +-
 .../language/simple/SimpleOperatorConstants.java   | 232 +++++++++++++++++++++
 .../org/apache/camel/tooling/model/JsonMapper.java |  65 ++++++
 .../apache/camel/tooling/model/LanguageModel.java  |  58 ++++++
 .../camel/maven/packaging/PackageLanguageMojo.java |  54 +++++
 .../org/apache/camel/spi/annotations/Language.java |  13 ++
 8 files changed, 470 insertions(+), 1 deletion(-)

diff --git 
a/core/camel-api/src/generated/java/org/apache/camel/spi/annotations/Language.java
 
b/core/camel-api/src/generated/java/org/apache/camel/spi/annotations/Language.java
index 5dc93f9f4b60..a45a9f2cef2d 100644
--- 
a/core/camel-api/src/generated/java/org/apache/camel/spi/annotations/Language.java
+++ 
b/core/camel-api/src/generated/java/org/apache/camel/spi/annotations/Language.java
@@ -43,4 +43,17 @@ public @interface Language {
      */
     Class<?> functionsClass() default void.class;
 
+    /**
+     * The class that contains all the name of operators that are supported by 
the language. The name of the operators
+     * are defined as {@code String} constants in the operators class.
+     *
+     * The class to provide can be any class but by convention, we would 
expect a class whose name is of type
+     * <i>xxxOperatorConstants</i> where <i>xxx</i> is the name of the 
corresponding language like for example
+     * <i>SimpleOperatorConstants</i> for the language <i>camel-simple</i>.
+     *
+     * The metadata of a given operator are retrieved directly from the 
annotation {@code @Metadata} added to the
+     * {@code String} constant representing its name and defined in the 
operators class.
+     */
+    Class<?> operatorsClass() default void.class;
+
 }
diff --git 
a/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json
 
b/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json
index 1c6720fc16c3..d7224071f387 100644
--- 
a/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json
+++ 
b/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json
@@ -154,5 +154,39 @@
     "variableAs(key,type)": { "index": 126, "kind": "function", "displayName": 
"Variable As", "group": "core", "label": "core", "required": false, "javaType": 
"Object", "prefix": "${", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Converts the variable to 
the given type (classname).", "ognl": false, "suffix": "}", "params": [ { 
"name": "key", "javaType": "String", "required": true, "description": "The 
variable name" }, { "name": "type",  [...]
     "variables": { "index": 127, "kind": "function", "displayName": 
"Variables", "group": "core", "label": "core", "required": false, "javaType": 
"java.util.Map", "prefix": "${", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Returns all the variables 
from the current Exchange in a Map", "ognl": false, "suffix": "}", "examples": 
[ "${variables} -> {myVar=value1, count=5}" ] },
     "xpath(input,exp)": { "index": 128, "kind": "function", "displayName": 
"XPath", "group": "xml", "label": "xml", "required": false, "javaType": 
"Object", "prefix": "${", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "When working with XML 
data, then this allows using the XPath language, for example, to extract data 
from the message body (in XML format). This requires having camel-xpath JAR on 
the classpath. For input (optional), you ca [...]
+  },
+  "operators": {
+    "==": { "index": 0, "kind": "operator", "displayName": "Eq", "label": 
"binary", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Tests equality between 
left and right operand values. Camel will coerce the right operand type to 
match the left.", "operatorKind": "binary", "operatorSyntax": "LHS == RHS", 
"precedence": 10, "examples": [ "${header.foo} == 'bar'", "${header.count} == 
5" ] },
+    "=~": { "index": 1, "kind": "operator", "displayName": "Eq ignore", 
"label": "binary", "required": false, "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "Tests equality between 
left and right operand values, ignoring case for string comparison.", 
"operatorKind": "binary", "operatorSyntax": "LHS =~ RHS", "precedence": 10, 
"examples": [ "${header.foo} =~ 'BAR'" ] },
+    ">": { "index": 2, "kind": "operator", "displayName": "Gt", "label": 
"binary", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Tests whether the left 
operand is greater than the right operand.", "operatorKind": "binary", 
"operatorSyntax": "LHS > RHS", "precedence": 10, "examples": [ "${header.count} 
> 100" ] },
+    ">=": { "index": 3, "kind": "operator", "displayName": "Gte", "label": 
"binary", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Tests whether the left 
operand is greater than or equal to the right operand.", "operatorKind": 
"binary", "operatorSyntax": "LHS >= RHS", "precedence": 10, "examples": [ 
"${header.count} >= 100" ] },
+    "<": { "index": 4, "kind": "operator", "displayName": "Lt", "label": 
"binary", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Tests whether the left 
operand is less than the right operand.", "operatorKind": "binary", 
"operatorSyntax": "LHS < RHS", "precedence": 10, "examples": [ "${header.count} 
< 100" ] },
+    "<=": { "index": 5, "kind": "operator", "displayName": "Lte", "label": 
"binary", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Tests whether the left 
operand is less than or equal to the right operand.", "operatorKind": "binary", 
"operatorSyntax": "LHS <= RHS", "precedence": 10, "examples": [ 
"${header.count} <= 100" ] },
+    "!=": { "index": 6, "kind": "operator", "displayName": "Not eq", "label": 
"binary", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Tests inequality between 
left and right operand values.", "operatorKind": "binary", "operatorSyntax": 
"LHS != RHS", "precedence": 10, "examples": [ "${header.foo} != 'bar'" ] },
+    "!=~": { "index": 7, "kind": "operator", "displayName": "Not eq ignore", 
"label": "binary", "required": false, "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "Tests inequality 
between left and right operand values, ignoring case for string comparison.", 
"operatorKind": "binary", "operatorSyntax": "LHS !=~ RHS", "precedence": 10, 
"examples": [ "${header.foo} !=~ 'BAR'" ] },
+    "contains": { "index": 8, "kind": "operator", "displayName": "Contains", 
"label": "binary", "required": false, "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "Tests whether the left 
operand string contains the right operand string.", "operatorKind": "binary", 
"operatorSyntax": "LHS contains RHS", "precedence": 10, "examples": [ 
"${header.title} contains 'Camel'" ] },
+    "!contains": { "index": 9, "kind": "operator", "displayName": "Not 
contains", "label": "binary", "required": false, "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "description": 
"Tests whether the left operand string does not contain the right operand 
string.", "operatorKind": "binary", "operatorSyntax": "LHS !contains RHS", 
"precedence": 10, "examples": [ "${header.title} !contains 'Camel'" ] },
+    "~~": { "index": 10, "kind": "operator", "displayName": "Contains 
ignorecase", "label": "binary", "required": false, "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "description": 
"Tests whether the left operand string contains the right operand string, 
ignoring case.", "operatorKind": "binary", "operatorSyntax": "LHS ~~ RHS", 
"precedence": 10, "examples": [ "${header.title} ~~ 'camel'" ] },
+    "!~~": { "index": 11, "kind": "operator", "displayName": "Not contains 
ignorecase", "label": "binary", "required": false, "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "description": 
"Tests whether the left operand string does not contain the right operand 
string, ignoring case.", "operatorKind": "binary", "operatorSyntax": "LHS !~~ 
RHS", "precedence": 10, "examples": [ "${header.title} !~~ 'camel'" ] },
+    "regex": { "index": 12, "kind": "operator", "displayName": "Regex", 
"label": "binary", "required": false, "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "Tests whether the left 
operand matches the right operand as a regular expression.", "operatorKind": 
"binary", "operatorSyntax": "LHS regex 'pattern'", "precedence": 10, 
"examples": [ "${header.number} regex '\\d{4}'" ] },
+    "!regex": { "index": 13, "kind": "operator", "displayName": "Not regex", 
"label": "binary", "required": false, "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "Tests whether the left 
operand does not match the right operand as a regular expression.", 
"operatorKind": "binary", "operatorSyntax": "LHS !regex 'pattern'", 
"precedence": 10, "examples": [ "${header.number} !regex '\\d{4}'" ] },
+    "in": { "index": 14, "kind": "operator", "displayName": "In", "label": 
"binary", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Tests whether the left 
operand is in a set of comma-separated values.", "operatorKind": "binary", 
"operatorSyntax": "LHS in 'val1,val2,...'", "precedence": 10, "examples": [ 
"${header.type} in 'gold,silver'" ] },
+    "!in": { "index": 15, "kind": "operator", "displayName": "Not in", 
"label": "binary", "required": false, "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "Tests whether the left 
operand is not in a set of comma-separated values.", "operatorKind": "binary", 
"operatorSyntax": "LHS !in 'val1,val2,...'", "precedence": 10, "examples": [ 
"${header.type} !in 'gold,silver'" ] },
+    "is": { "index": 16, "kind": "operator", "displayName": "Is", "label": 
"binary", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Tests whether the left 
operand is an instance of the right operand type (Java classname or short 
name).", "operatorKind": "binary", "operatorSyntax": "LHS is 'typeName'", 
"precedence": 10, "examples": [ "${header.type} is 'String'", "${body} is 
'java.util.List'" ] },
+    "!is": { "index": 17, "kind": "operator", "displayName": "Not is", 
"label": "binary", "required": false, "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "Tests whether the left 
operand is not an instance of the right operand type.", "operatorKind": 
"binary", "operatorSyntax": "LHS !is 'typeName'", "precedence": 10, "examples": 
[ "${header.type} !is 'String'" ] },
+    "range": { "index": 18, "kind": "operator", "displayName": "Range", 
"label": "binary", "required": false, "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "Tests whether the left 
operand is within the numeric range specified by 'from..to'.", "operatorKind": 
"binary", "operatorSyntax": "LHS range 'from..to'", "precedence": 10, 
"examples": [ "${header.number} range '100..199'" ] },
+    "!range": { "index": 19, "kind": "operator", "displayName": "Not range", 
"label": "binary", "required": false, "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "Tests whether the left 
operand is not within the numeric range specified by 'from..to'.", 
"operatorKind": "binary", "operatorSyntax": "LHS !range 'from..to'", 
"precedence": 10, "examples": [ "${header.number} !range '100..199'" ] },
+    "startsWith": { "index": 20, "kind": "operator", "displayName": "Starts 
with", "label": "binary", "required": false, "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "description": 
"Tests whether the left operand string starts with the right operand string.", 
"operatorKind": "binary", "operatorSyntax": "LHS startsWith RHS", "precedence": 
10, "examples": [ "${header.name} startsWith 'Camel'" ] },
+    "!startsWith": { "index": 21, "kind": "operator", "displayName": "Not 
starts with", "label": "binary", "required": false, "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "description": 
"Tests whether the left operand string does not start with the right operand 
string.", "operatorKind": "binary", "operatorSyntax": "LHS !startsWith RHS", 
"precedence": 10, "examples": [ "${header.name} !startsWith 'Camel'" ] },
+    "endsWith": { "index": 22, "kind": "operator", "displayName": "Ends with", 
"label": "binary", "required": false, "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "Tests whether the left 
operand string ends with the right operand string.", "operatorKind": "binary", 
"operatorSyntax": "LHS endsWith RHS", "precedence": 10, "examples": [ 
"${header.name} endsWith '.xml'" ] },
+    "!endsWith": { "index": 23, "kind": "operator", "displayName": "Not ends 
with", "label": "binary", "required": false, "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "description": 
"Tests whether the left operand string does not end with the right operand 
string.", "operatorKind": "binary", "operatorSyntax": "LHS !endsWith RHS", 
"precedence": 10, "examples": [ "${header.name} !endsWith '.xml'" ] },
+    "++": { "index": 24, "kind": "operator", "displayName": "Inc", "label": 
"unary", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Increments the numeric 
value by one. Must immediately follow a function closing brace.", 
"operatorKind": "unary", "operatorSyntax": "${fn}++", "precedence": 1, 
"examples": [ "${header.count}++" ] },
+    "--": { "index": 25, "kind": "operator", "displayName": "Dec", "label": 
"unary", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Decrements the numeric 
value by one. Must immediately follow a function closing brace.", 
"operatorKind": "unary", "operatorSyntax": "${fn}--", "precedence": 1, 
"examples": [ "${header.count}--" ] },
+    "&&": { "index": 26, "kind": "operator", "displayName": "And", "label": 
"logical", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Logical AND. Both left and 
right predicates must evaluate to true.", "operatorKind": "logical", 
"operatorSyntax": "predicate && predicate", "precedence": 30, "examples": [ 
"${header.title} contains 'Camel' && ${header.type} == 'gold'" ] },
+    "||": { "index": 27, "kind": "operator", "displayName": "Or", "label": 
"logical", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Logical OR. At least one 
of the left or right predicates must evaluate to true.", "operatorKind": 
"logical", "operatorSyntax": "predicate || predicate", "precedence": 30, 
"examples": [ "${header.title} contains 'Camel' || ${header.type} == 'gold'" ] 
},
+    "? :": { "index": 28, "kind": "operator", "displayName": "Ternary", 
"label": "ternary", "required": false, "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "Ternary conditional 
operator. Evaluates the predicate and returns trueValue if true, falseValue if 
false. Requires spaces around both ? and : tokens.", "operatorKind": "ternary", 
"operatorSyntax": "predicate ? trueValue : falseValue", "precedence": 25, 
"examples": [ "${header.foo} >  [...]
+    "~>": { "index": 29, "kind": "operator", "displayName": "Chain", "label": 
"chain", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Pipes the result of the 
left expression as input body to the right expression. Use $param in the right 
expression to reference the piped value explicitly.", "operatorKind": "chain", 
"operatorSyntax": "expr ~> expr", "precedence": 5, "examples": [ "${trim()} ~> 
${uppercase()}", "${substrin [...]
+    "?~>": { "index": 30, "kind": "operator", "displayName": "Chain null 
safe", "label": "chain", "required": false, "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "description": 
"Null-safe chain operator. Same as ~> but stops chaining and returns null if 
the left expression evaluates to null.", "operatorKind": "chain", 
"operatorSyntax": "expr ?~> expr", "precedence": 5, "examples": [ 
"${header.name} ?~> ${trim()} ?~> ${uppercase()}" ] },
+    "?:": { "index": 31, "kind": "operator", "displayName": "Elvis", "label": 
"other", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Elvis operator 
(null-coalescing). Returns the left operand if it is not null\/empty, otherwise 
returns the right operand as a fallback value.", "operatorKind": "other", 
"operatorSyntax": "expr ?: defaultValue", "precedence": 20, "examples": [ 
"${header.username} ?: 'Guest'", "${body} ?: $ [...]
   }
 }
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleLanguage.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleLanguage.java
index 4be673a39504..c199f275b586 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleLanguage.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleLanguage.java
@@ -39,7 +39,7 @@ import org.slf4j.LoggerFactory;
 /**
  * The Camel simple language.
  */
-@Language(value = "simple", functionsClass = SimpleConstants.class)
+@Language(value = "simple", functionsClass = SimpleConstants.class, 
operatorsClass = SimpleOperatorConstants.class)
 public class SimpleLanguage extends LanguageSupport implements StaticService {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(SimpleLanguage.class);
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleOperatorConstants.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleOperatorConstants.java
new file mode 100644
index 000000000000..dfbca7987d91
--- /dev/null
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleOperatorConstants.java
@@ -0,0 +1,232 @@
+/*
+ * 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.language.simple;
+
+import org.apache.camel.spi.Metadata;
+
+@Metadata(label = "operator")
+public final class SimpleOperatorConstants {
+
+    // --- Binary comparison operators (precedence 10) ---
+
+    @Metadata(description = "Tests equality between left and right operand 
values. Camel will coerce the right operand type to match the left.",
+              label = "binary",
+              examples = { "${header.foo} == 'bar'", "${header.count} == 5" },
+              annotations = { "kind=binary", "syntax=LHS == RHS", 
"precedence=10" })
+    public static final String EQ = "==";
+
+    @Metadata(description = "Tests equality between left and right operand 
values, ignoring case for string comparison.",
+              label = "binary",
+              examples = { "${header.foo} =~ 'BAR'" },
+              annotations = { "kind=binary", "syntax=LHS =~ RHS", 
"precedence=10" })
+    public static final String EQ_IGNORE = "=~";
+
+    @Metadata(description = "Tests whether the left operand is greater than 
the right operand.",
+              label = "binary",
+              examples = { "${header.count} > 100" },
+              annotations = { "kind=binary", "syntax=LHS > RHS", 
"precedence=10" })
+    public static final String GT = ">";
+
+    @Metadata(description = "Tests whether the left operand is greater than or 
equal to the right operand.",
+              label = "binary",
+              examples = { "${header.count} >= 100" },
+              annotations = { "kind=binary", "syntax=LHS >= RHS", 
"precedence=10" })
+    public static final String GTE = ">=";
+
+    @Metadata(description = "Tests whether the left operand is less than the 
right operand.",
+              label = "binary",
+              examples = { "${header.count} < 100" },
+              annotations = { "kind=binary", "syntax=LHS < RHS", 
"precedence=10" })
+    public static final String LT = "<";
+
+    @Metadata(description = "Tests whether the left operand is less than or 
equal to the right operand.",
+              label = "binary",
+              examples = { "${header.count} <= 100" },
+              annotations = { "kind=binary", "syntax=LHS <= RHS", 
"precedence=10" })
+    public static final String LTE = "<=";
+
+    @Metadata(description = "Tests inequality between left and right operand 
values.",
+              label = "binary",
+              examples = { "${header.foo} != 'bar'" },
+              annotations = { "kind=binary", "syntax=LHS != RHS", 
"precedence=10" })
+    public static final String NOT_EQ = "!=";
+
+    @Metadata(description = "Tests inequality between left and right operand 
values, ignoring case for string comparison.",
+              label = "binary",
+              examples = { "${header.foo} !=~ 'BAR'" },
+              annotations = { "kind=binary", "syntax=LHS !=~ RHS", 
"precedence=10" })
+    public static final String NOT_EQ_IGNORE = "!=~";
+
+    @Metadata(description = "Tests whether the left operand string contains 
the right operand string.",
+              label = "binary",
+              examples = { "${header.title} contains 'Camel'" },
+              annotations = { "kind=binary", "syntax=LHS contains RHS", 
"precedence=10" })
+    public static final String CONTAINS = "contains";
+
+    @Metadata(description = "Tests whether the left operand string does not 
contain the right operand string.",
+              label = "binary",
+              examples = { "${header.title} !contains 'Camel'" },
+              annotations = { "kind=binary", "syntax=LHS !contains RHS", 
"precedence=10" })
+    public static final String NOT_CONTAINS = "!contains";
+
+    @Metadata(description = "Tests whether the left operand string contains 
the right operand string, ignoring case.",
+              label = "binary",
+              examples = { "${header.title} ~~ 'camel'" },
+              annotations = { "kind=binary", "syntax=LHS ~~ RHS", 
"precedence=10" })
+    public static final String CONTAINS_IGNORECASE = "~~";
+
+    @Metadata(description = "Tests whether the left operand string does not 
contain the right operand string, ignoring case.",
+              label = "binary",
+              examples = { "${header.title} !~~ 'camel'" },
+              annotations = { "kind=binary", "syntax=LHS !~~ RHS", 
"precedence=10" })
+    public static final String NOT_CONTAINS_IGNORECASE = "!~~";
+
+    @Metadata(description = "Tests whether the left operand matches the right 
operand as a regular expression.",
+              label = "binary",
+              examples = { "${header.number} regex '\\d{4}'" },
+              annotations = { "kind=binary", "syntax=LHS regex 'pattern'", 
"precedence=10" })
+    public static final String REGEX = "regex";
+
+    @Metadata(description = "Tests whether the left operand does not match the 
right operand as a regular expression.",
+              label = "binary",
+              examples = { "${header.number} !regex '\\d{4}'" },
+              annotations = { "kind=binary", "syntax=LHS !regex 'pattern'", 
"precedence=10" })
+    public static final String NOT_REGEX = "!regex";
+
+    @Metadata(description = "Tests whether the left operand is in a set of 
comma-separated values.",
+              label = "binary",
+              examples = { "${header.type} in 'gold,silver'" },
+              annotations = { "kind=binary", "syntax=LHS in 'val1,val2,...'", 
"precedence=10" })
+    public static final String IN = "in";
+
+    @Metadata(description = "Tests whether the left operand is not in a set of 
comma-separated values.",
+              label = "binary",
+              examples = { "${header.type} !in 'gold,silver'" },
+              annotations = { "kind=binary", "syntax=LHS !in 'val1,val2,...'", 
"precedence=10" })
+    public static final String NOT_IN = "!in";
+
+    @Metadata(description = "Tests whether the left operand is an instance of 
the right operand type (Java classname or short name).",
+              label = "binary",
+              examples = { "${header.type} is 'String'", "${body} is 
'java.util.List'" },
+              annotations = { "kind=binary", "syntax=LHS is 'typeName'", 
"precedence=10" })
+    public static final String IS = "is";
+
+    @Metadata(description = "Tests whether the left operand is not an instance 
of the right operand type.",
+              label = "binary",
+              examples = { "${header.type} !is 'String'" },
+              annotations = { "kind=binary", "syntax=LHS !is 'typeName'", 
"precedence=10" })
+    public static final String NOT_IS = "!is";
+
+    @Metadata(description = "Tests whether the left operand is within the 
numeric range specified by 'from..to'.",
+              label = "binary",
+              examples = { "${header.number} range '100..199'" },
+              annotations = { "kind=binary", "syntax=LHS range 'from..to'", 
"precedence=10" })
+    public static final String RANGE = "range";
+
+    @Metadata(description = "Tests whether the left operand is not within the 
numeric range specified by 'from..to'.",
+              label = "binary",
+              examples = { "${header.number} !range '100..199'" },
+              annotations = { "kind=binary", "syntax=LHS !range 'from..to'", 
"precedence=10" })
+    public static final String NOT_RANGE = "!range";
+
+    @Metadata(description = "Tests whether the left operand string starts with 
the right operand string.",
+              label = "binary",
+              examples = { "${header.name} startsWith 'Camel'" },
+              annotations = { "kind=binary", "syntax=LHS startsWith RHS", 
"precedence=10" })
+    public static final String STARTS_WITH = "startsWith";
+
+    @Metadata(description = "Tests whether the left operand string does not 
start with the right operand string.",
+              label = "binary",
+              examples = { "${header.name} !startsWith 'Camel'" },
+              annotations = { "kind=binary", "syntax=LHS !startsWith RHS", 
"precedence=10" })
+    public static final String NOT_STARTS_WITH = "!startsWith";
+
+    @Metadata(description = "Tests whether the left operand string ends with 
the right operand string.",
+              label = "binary",
+              examples = { "${header.name} endsWith '.xml'" },
+              annotations = { "kind=binary", "syntax=LHS endsWith RHS", 
"precedence=10" })
+    public static final String ENDS_WITH = "endsWith";
+
+    @Metadata(description = "Tests whether the left operand string does not 
end with the right operand string.",
+              label = "binary",
+              examples = { "${header.name} !endsWith '.xml'" },
+              annotations = { "kind=binary", "syntax=LHS !endsWith RHS", 
"precedence=10" })
+    public static final String NOT_ENDS_WITH = "!endsWith";
+
+    // --- Unary operators (precedence 1) ---
+
+    @Metadata(description = "Increments the numeric value by one. Must 
immediately follow a function closing brace.",
+              label = "unary",
+              examples = { "${header.count}++" },
+              annotations = { "kind=unary", "syntax=${fn}++", "precedence=1" })
+    public static final String INC = "++";
+
+    @Metadata(description = "Decrements the numeric value by one. Must 
immediately follow a function closing brace.",
+              label = "unary",
+              examples = { "${header.count}--" },
+              annotations = { "kind=unary", "syntax=${fn}--", "precedence=1" })
+    public static final String DEC = "--";
+
+    // --- Logical operators (precedence 30) ---
+
+    @Metadata(description = "Logical AND. Both left and right predicates must 
evaluate to true.",
+              label = "logical",
+              examples = { "${header.title} contains 'Camel' && ${header.type} 
== 'gold'" },
+              annotations = { "kind=logical", "syntax=predicate && predicate", 
"precedence=30" })
+    public static final String AND = "&&";
+
+    @Metadata(description = "Logical OR. At least one of the left or right 
predicates must evaluate to true.",
+              label = "logical",
+              examples = { "${header.title} contains 'Camel' || ${header.type} 
== 'gold'" },
+              annotations = { "kind=logical", "syntax=predicate || predicate", 
"precedence=30" })
+    public static final String OR = "||";
+
+    // --- Ternary operator (precedence 25) ---
+
+    @Metadata(description = "Ternary conditional operator. Evaluates the 
predicate and returns trueValue if true, falseValue if false. Requires spaces 
around both ? and : tokens.",
+              label = "ternary",
+              examples = {
+                      "${header.foo} > 0 ? 'positive' : 'negative'",
+                      "${header.score} >= 90 ? 'A' : ${header.score} >= 80 ? 
'B' : 'C'" },
+              annotations = { "kind=ternary", "syntax=predicate ? trueValue : 
falseValue", "precedence=25" })
+    public static final String TERNARY = "? :";
+
+    // --- Chain operators (precedence 5) ---
+
+    @Metadata(description = "Pipes the result of the left expression as input 
body to the right expression. Use $param in the right expression to reference 
the piped value explicitly.",
+              label = "chain",
+              examples = { "${trim()} ~> ${uppercase()}", 
"${substringAfter('Hello')} ~> ${trim()} ~> ${uppercase()}" },
+              annotations = { "kind=chain", "syntax=expr ~> expr", 
"precedence=5" })
+    public static final String CHAIN = "~>";
+
+    @Metadata(description = "Null-safe chain operator. Same as ~> but stops 
chaining and returns null if the left expression evaluates to null.",
+              label = "chain",
+              examples = { "${header.name} ?~> ${trim()} ?~> ${uppercase()}" },
+              annotations = { "kind=chain", "syntax=expr ?~> expr", 
"precedence=5" })
+    public static final String CHAIN_NULL_SAFE = "?~>";
+
+    // --- Other operators (precedence 20) ---
+
+    @Metadata(description = "Elvis operator (null-coalescing). Returns the 
left operand if it is not null/empty, otherwise returns the right operand as a 
fallback value.",
+              label = "other",
+              examples = { "${header.username} ?: 'Guest'", "${body} ?: 
${header.default}" },
+              annotations = { "kind=other", "syntax=expr ?: defaultValue", 
"precedence=20" })
+    public static final String ELVIS = "?:";
+
+    private SimpleOperatorConstants() {
+    }
+}
diff --git 
a/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/JsonMapper.java
 
b/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/JsonMapper.java
index b5759e911672..7d87db4c33c1 100644
--- 
a/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/JsonMapper.java
+++ 
b/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/JsonMapper.java
@@ -414,6 +414,15 @@ public final class JsonMapper {
                 model.addFunction(func);
             }
         }
+        JsonObject mpro = (JsonObject) obj.get("operators");
+        if (mpro != null) {
+            for (Map.Entry<String, Object> entry : mpro.entrySet()) {
+                JsonObject mp = (JsonObject) entry.getValue();
+                LanguageModel.LanguageOperatorModel op = new 
LanguageModel.LanguageOperatorModel();
+                parseOperator(mp, op, entry.getKey());
+                model.addOperator(op);
+            }
+        }
         return model;
     }
 
@@ -436,6 +445,10 @@ public final class JsonMapper {
         if (!functions.isEmpty()) {
             wrapper.put("functions", asJsonObjectFunctions(functions));
         }
+        final List<LanguageModel.LanguageOperatorModel> operators = 
model.getOperators();
+        if (!operators.isEmpty()) {
+            wrapper.put("operators", asJsonObjectOperators(operators));
+        }
         return wrapper;
     }
 
@@ -667,6 +680,37 @@ public final class JsonMapper {
         }
     }
 
+    private static void parseOperator(JsonObject mp, 
LanguageModel.LanguageOperatorModel op, String name) {
+        op.setName(name);
+        op.setConstantName(name);
+        Integer idx = mp.getInteger("index");
+        if (idx != null) {
+            op.setIndex(idx);
+        }
+        op.setKind(mp.getString("kind"));
+        op.setDisplayName(mp.getString("displayName"));
+        op.setGroup(mp.getString("group"));
+        op.setLabel(mp.getString("label"));
+        op.setJavaType(mp.getString("javaType"));
+        op.setDeprecated(mp.getBooleanOrDefault("deprecated", false));
+        op.setDeprecationNote(mp.getString("deprecationNote"));
+        op.setDescription(mp.getString("description"));
+        op.setOperatorKind(mp.getString("operatorKind"));
+        op.setOperatorSyntax(mp.getString("operatorSyntax"));
+        Integer prec = mp.getInteger("precedence");
+        if (prec != null) {
+            op.setPrecedence(prec);
+        }
+        Object examplesObj = mp.get("examples");
+        if (examplesObj instanceof JsonArray examplesArr) {
+            for (Object e : examplesArr) {
+                if (e instanceof String s) {
+                    op.addExample(s);
+                }
+            }
+        }
+    }
+
     public static JsonObject asJsonObject(List<? extends BaseOptionModel> 
options) {
         JsonObject json = new JsonObject();
         for (int i = 0; i < options.size(); i++) {
@@ -717,6 +761,27 @@ public final class JsonMapper {
         return json;
     }
 
+    public static JsonObject 
asJsonObjectOperators(List<LanguageModel.LanguageOperatorModel> options) {
+        JsonObject json = new JsonObject();
+        for (int i = 0; i < options.size(); i++) {
+            var o = options.get(i);
+            o.setIndex(i);
+            JsonObject jo = asJsonObject(o);
+            if (o.getOperatorKind() != null) {
+                jo.put("operatorKind", o.getOperatorKind());
+            }
+            if (o.getOperatorSyntax() != null) {
+                jo.put("operatorSyntax", o.getOperatorSyntax());
+            }
+            jo.put("precedence", o.getPrecedence());
+            if (!o.getExamples().isEmpty()) {
+                jo.put("examples", new JsonArray(o.getExamples()));
+            }
+            json.put(o.getName(), jo);
+        }
+        return json;
+    }
+
     public static JsonObject apiModelAsJsonObject(Collection<ApiModel> model, 
boolean options) {
         JsonObject root = new JsonObject();
         model.forEach(a -> {
diff --git 
a/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/LanguageModel.java
 
b/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/LanguageModel.java
index e2264dbfafe8..ce1ba4a1e118 100644
--- 
a/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/LanguageModel.java
+++ 
b/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/LanguageModel.java
@@ -24,6 +24,7 @@ public class LanguageModel extends 
ArtifactModel<LanguageModel.LanguageOptionMod
     protected String modelName;
     protected String modelJavaType;
     protected final List<LanguageFunctionModel> functions = new ArrayList<>();
+    protected final List<LanguageOperatorModel> operators = new ArrayList<>();
 
     public static class LanguageOptionModel extends BaseOptionModel {
 
@@ -61,6 +62,14 @@ public class LanguageModel extends 
ArtifactModel<LanguageModel.LanguageOptionMod
         functions.add(function);
     }
 
+    public List<LanguageOperatorModel> getOperators() {
+        return operators;
+    }
+
+    public void addOperator(LanguageOperatorModel operator) {
+        operators.add(operator);
+    }
+
     public static class LanguageFunctionModel extends BaseOptionModel {
 
         /**
@@ -144,6 +153,55 @@ public class LanguageModel extends 
ArtifactModel<LanguageModel.LanguageOptionMod
         }
     }
 
+    public static class LanguageOperatorModel extends BaseOptionModel {
+
+        private String constantName;
+        private String operatorKind;
+        private String operatorSyntax;
+        private int precedence;
+        private final List<String> examples = new ArrayList<>();
+
+        public String getConstantName() {
+            return constantName;
+        }
+
+        public void setConstantName(String constantName) {
+            this.constantName = constantName;
+        }
+
+        public String getOperatorKind() {
+            return operatorKind;
+        }
+
+        public void setOperatorKind(String operatorKind) {
+            this.operatorKind = operatorKind;
+        }
+
+        public String getOperatorSyntax() {
+            return operatorSyntax;
+        }
+
+        public void setOperatorSyntax(String operatorSyntax) {
+            this.operatorSyntax = operatorSyntax;
+        }
+
+        public int getPrecedence() {
+            return precedence;
+        }
+
+        public void setPrecedence(int precedence) {
+            this.precedence = precedence;
+        }
+
+        public List<String> getExamples() {
+            return examples;
+        }
+
+        public void addExample(String example) {
+            examples.add(example);
+        }
+    }
+
     public static class FunctionParamModel {
         private String name;
         private String javaType;
diff --git 
a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PackageLanguageMojo.java
 
b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PackageLanguageMojo.java
index 00754c6d4c39..7ae51a8bdedc 100644
--- 
a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PackageLanguageMojo.java
+++ 
b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PackageLanguageMojo.java
@@ -286,6 +286,24 @@ public class PackageLanguageMojo extends 
AbstractGeneratorMojo {
             }
         }
 
+        // read class and find operatorsClass and add each field as an 
operator in the model
+        if (lan.operatorsClass() != void.class) {
+            classElement = loadClass(lan.operatorsClass().getName());
+            if (classElement != null) {
+                for (Field field : classElement.getFields()) {
+                    final boolean isEnum = classElement.isEnum();
+                    if ((isEnum || isStatic(field.getModifiers()) && 
field.getType() == String.class)
+                            && field.isAnnotationPresent(Metadata.class)) {
+                        try {
+                            addOperator(model, field);
+                        } catch (Exception e) {
+                            getLog().warn(e);
+                        }
+                    }
+                }
+            }
+        }
+
         return model;
     }
 
@@ -370,6 +388,42 @@ public class PackageLanguageMojo extends 
AbstractGeneratorMojo {
         model.addFunction(fun);
     }
 
+    private void addOperator(LanguageModel model, Field field) throws 
Exception {
+        final Metadata metadata = field.getAnnotation(Metadata.class);
+        LanguageModel.LanguageOperatorModel op = new 
LanguageModel.LanguageOperatorModel();
+        op.setConstantName(String.format("%s#%s", 
field.getDeclaringClass().getName(), field.getName()));
+        op.setName((String) field.get(null));
+        op.setDescription(metadata.description().trim());
+        op.setKind("operator");
+        String displayName = metadata.displayName();
+        if (Strings.isNullOrEmpty(displayName)) {
+            displayName = Strings.asTitle(field.getName().replace('_', ' 
').toLowerCase());
+        }
+        op.setDisplayName(displayName);
+        op.setDeprecated(field.isAnnotationPresent(Deprecated.class));
+        op.setDeprecationNote(metadata.deprecationNote());
+        op.setLabel(metadata.label());
+        if (metadata.examples() != null) {
+            for (String ex : metadata.examples()) {
+                if (!Strings.isNullOrEmpty(ex)) {
+                    op.addExample(ex.trim());
+                }
+            }
+        }
+        if (metadata.annotations() != null) {
+            for (String s : metadata.annotations()) {
+                if (s.startsWith("kind=")) {
+                    op.setOperatorKind(s.substring(5));
+                } else if (s.startsWith("syntax=")) {
+                    op.setOperatorSyntax(s.substring(7));
+                } else if (s.startsWith("precedence=")) {
+                    op.setPrecedence(Integer.parseInt(s.substring(11)));
+                }
+            }
+        }
+        model.addOperator(op);
+    }
+
     private static LanguageModel.FunctionParamModel parseParam(String value) {
         // format: name:javaType:required|optional:defaultValue:description
         String[] parts = value.split(":", 5);
diff --git 
a/tooling/spi-annotations/src/main/java/org/apache/camel/spi/annotations/Language.java
 
b/tooling/spi-annotations/src/main/java/org/apache/camel/spi/annotations/Language.java
index 5dc93f9f4b60..a45a9f2cef2d 100644
--- 
a/tooling/spi-annotations/src/main/java/org/apache/camel/spi/annotations/Language.java
+++ 
b/tooling/spi-annotations/src/main/java/org/apache/camel/spi/annotations/Language.java
@@ -43,4 +43,17 @@ public @interface Language {
      */
     Class<?> functionsClass() default void.class;
 
+    /**
+     * The class that contains all the name of operators that are supported by 
the language. The name of the operators
+     * are defined as {@code String} constants in the operators class.
+     *
+     * The class to provide can be any class but by convention, we would 
expect a class whose name is of type
+     * <i>xxxOperatorConstants</i> where <i>xxx</i> is the name of the 
corresponding language like for example
+     * <i>SimpleOperatorConstants</i> for the language <i>camel-simple</i>.
+     *
+     * The metadata of a given operator are retrieved directly from the 
annotation {@code @Metadata} added to the
+     * {@code String} constant representing its name and defined in the 
operators class.
+     */
+    Class<?> operatorsClass() default void.class;
+
 }


Reply via email to