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

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 72a1bff606a3 CAMEL-25928: runtime checks of the body types of the EIPs 
implemented outside camel-core (#28451)
72a1bff606a3 is described below

commit 72a1bff606a3349674d027368132e30fe02aebc0
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Oct 25 15:28:01 2026 +0100

    CAMEL-25928: runtime checks of the body types of the EIPs implemented 
outside camel-core (#28451)
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../component/a2a/A2ASubTaskBodyTypesTest.java     |  94 ++++++++++++
 .../tokenizer/TokenizerBodyTypesTest.java          |  99 ++++++++++++
 .../component/kamelet/KameletBodyTypesTest.java    |  90 +++++++++++
 .../FaultToleranceBodyTypesTest.java               | 165 ++++++++++++++++++++
 .../resilience4j/CircuitBreakerBodyTypesTest.java  | 170 +++++++++++++++++++++
 5 files changed, 618 insertions(+)

diff --git 
a/components/camel-ai/camel-a2a/src/test/java/org/apache/camel/component/a2a/A2ASubTaskBodyTypesTest.java
 
b/components/camel-ai/camel-a2a/src/test/java/org/apache/camel/component/a2a/A2ASubTaskBodyTypesTest.java
new file mode 100644
index 000000000000..de51d207b681
--- /dev/null
+++ 
b/components/camel-ai/camel-a2a/src/test/java/org/apache/camel/component/a2a/A2ASubTaskBodyTypesTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.component.a2a;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.model.A2ASubTaskDefinition;
+import org.apache.camel.spi.BodyType.Carrier;
+import org.apache.camel.spi.BodyTypes;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The body types of the a2aSubTask EIP (CAMEL-25928), checked at runtime: its 
steps start with the body as it was, and
+ * the body after it is what its steps end with.
+ */
+class A2ASubTaskBodyTypesTest extends CamelTestSupport {
+
+    /** The body the steps end with. */
+    private static final String END = "end of the steps";
+
+    private final List<Object> inside = new CopyOnWriteArrayList<>();
+
+    A2ASubTaskBodyTypesTest() {
+        testConfiguration().withUseRouteBuilder(false);
+    }
+
+    @Test
+    void theStepsStartWithTheBodyAndTheBodyAfterIsWhatTheyEndWith() throws 
Exception {
+        BodyTypes bodyTypes = 
A2ASubTaskDefinition.class.getAnnotation(BodyTypes.class);
+        assertThat(bodyTypes).isNotNull();
+        assertThat(bodyTypes.eipOut().from()).isEqualTo("steps");
+        
assertThat(bodyTypes.eipSteps().value()).containsExactly(Carrier.UNCHANGED);
+
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:end")
+                        .a2aSubTask()
+                        .emitBefore("Before ${body}")
+                        .emitAfter("After ${body}")
+                        .process(e -> inside.add(e.getMessage().getBody()))
+                        .setBody(constant(END))
+                        .end();
+
+                from("direct:keep")
+                        .a2aSubTask()
+                        .emitBefore("Before ${body}")
+                        .emitAfter("After ${body}")
+                        .process(e -> inside.add(e.getMessage().getBody()))
+                        .end();
+            }
+        });
+        context.start();
+
+        Order body = new Order();
+        Exchange out = template.send("direct:end", e -> 
e.getMessage().setBody(body));
+        assertThat(out.getException()).isNull();
+        assertThat(inside).hasSize(1);
+        assertThat(inside.get(0)).isSameAs(body);
+        assertThat(out.getMessage().getBody()).isEqualTo(END);
+
+        // steps that leave the body as it was give the same instance after 
the EIP
+        inside.clear();
+        out = template.send("direct:keep", e -> e.getMessage().setBody(body));
+        assertThat(out.getException()).isNull();
+        assertThat(inside).hasSize(1);
+        assertThat(inside.get(0)).isSameAs(body);
+        assertThat(out.getMessage().getBody()).isSameAs(body);
+    }
+
+    /** A body that no converter knows: an EIP that keeps it gives the same 
instance. */
+    private static final class Order {
+    }
+}
diff --git 
a/components/camel-ai/camel-langchain4j-tokenizer/src/test/java/org/apache/camel/component/langchain4j/tokenizer/TokenizerBodyTypesTest.java
 
b/components/camel-ai/camel-langchain4j-tokenizer/src/test/java/org/apache/camel/component/langchain4j/tokenizer/TokenizerBodyTypesTest.java
new file mode 100644
index 000000000000..239e87699aea
--- /dev/null
+++ 
b/components/camel-ai/camel-langchain4j-tokenizer/src/test/java/org/apache/camel/component/langchain4j/tokenizer/TokenizerBodyTypesTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.component.langchain4j.tokenizer;
+
+import java.nio.charset.StandardCharsets;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.model.TokenizerDefinition;
+import org.apache.camel.model.tokenizer.LangChain4jTokenizerDefinition;
+import org.apache.camel.spi.BodyType;
+import org.apache.camel.spi.BodyType.Carrier;
+import org.apache.camel.spi.BodyTypes;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * The body types of the tokenizer EIP (CAMEL-25928), checked at runtime: it 
reads the body as text and leaves the
+ * chunks of the text as a String[].
+ */
+public class TokenizerBodyTypesTest extends LangChain4JTokenizerTestSupport {
+
+    @Test
+    public void textGivesTheChunksAsAnArray() {
+        BodyTypes bodyTypes = 
TokenizerDefinition.class.getAnnotation(BodyTypes.class);
+        assertNotNull(bodyTypes);
+        assertEquals(Carrier.TEXT, bodyTypes.eipIn().value()[0]);
+        BodyType out = bodyTypes.eipOut();
+        assertEquals(Carrier.LIST, out.value()[0]);
+        assertEquals("java.lang.String[]", out.javaType());
+
+        Exchange exchange = template.send("direct:start", e -> 
e.getMessage().setBody(TEXT));
+        assertNull(exchange.getException());
+        Object body = exchange.getMessage().getBody();
+        assertInstanceOf(String[].class, body);
+        assertEquals(out.javaType(), body.getClass().getCanonicalName());
+        assertEquals(4, ((String[]) body).length);
+    }
+
+    @Test
+    public void bytesAreReadAsText() {
+        // the tokenizer reads the body with getBody(String.class), so bytes 
are converted to text first
+        Exchange exchange
+                = template.send("direct:start", e -> 
e.getMessage().setBody(TEXT.getBytes(StandardCharsets.UTF_8)));
+        assertNull(exchange.getException());
+        Exchange text = template.send("direct:start", e -> 
e.getMessage().setBody(TEXT));
+        assertArrayEquals((String[]) text.getMessage().getBody(), (String[]) 
exchange.getMessage().getBody());
+    }
+
+    @Test
+    public void anObjectNoConverterKnowsIsReadAsItsToString() {
+        // no converter knows Order, but the fallback converter to String uses 
toString(), so the object is not
+        // refused: its toString() is what is split into chunks
+        Order order = new Order();
+        Exchange exchange = template.send("direct:start", e -> 
e.getMessage().setBody(order));
+        assertNull(exchange.getException());
+        assertArrayEquals(new String[] { order.toString() }, (String[]) 
exchange.getMessage().getBody());
+    }
+
+    @Override
+    protected RoutesBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:start")
+                        .tokenize(tokenizer()
+                                .byParagraph()
+                                .maxSegmentSize(1024)
+                                .maxOverlap(10)
+                                
.using(LangChain4jTokenizerDefinition.TokenizerType.OPEN_AI)
+                                .end());
+            }
+        };
+    }
+
+    /** A body that no converter knows. */
+    private static final class Order {
+    }
+}
diff --git 
a/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletBodyTypesTest.java
 
b/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletBodyTypesTest.java
new file mode 100644
index 000000000000..65d40d056e98
--- /dev/null
+++ 
b/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletBodyTypesTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.component.kamelet;
+
+import java.util.List;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.model.KameletDefinition;
+import org.apache.camel.spi.BodyType;
+import org.apache.camel.spi.BodyType.Carrier;
+import org.apache.camel.spi.BodyTypes;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The body types of the kamelet EIP (CAMEL-25928), checked at runtime. The 
catalog says UNKNOWN for the body after it,
+ * as it is what the route of the Kamelet leaves: the same EIP gives a String 
with one Kamelet, keeps the body with
+ * another and gives a List with a third.
+ */
+public class KameletBodyTypesTest extends CamelTestSupport {
+
+    @Test
+    public void theBodyAfterIsWhatTheKameletRouteLeaves() {
+        BodyTypes bodyTypes = 
KameletDefinition.class.getAnnotation(BodyTypes.class);
+        assertThat(bodyTypes).isNotNull();
+        BodyType out = bodyTypes.eipOut();
+        assertThat(out.value()).containsExactly(Carrier.UNKNOWN);
+        assertThat(out.from()).isEmpty();
+
+        Order body = new Order();
+
+        Exchange text = template.send("direct:text", e -> 
e.getMessage().setBody(body));
+        assertThat(text.getException()).isNull();
+        assertThat(text.getMessage().getBody()).isEqualTo("Hello from the 
Kamelet");
+
+        Exchange keep = template.send("direct:keep", e -> 
e.getMessage().setBody(body));
+        assertThat(keep.getException()).isNull();
+        assertThat(keep.getMessage().getBody()).isSameAs(body);
+
+        Exchange list = template.send("direct:list", e -> 
e.getMessage().setBody(body));
+        assertThat(list.getException()).isNull();
+        assertThat(list.getMessage().getBody()).isEqualTo(List.of("a", "b"));
+    }
+
+    @Override
+    protected RoutesBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                routeTemplate("text")
+                        .from("kamelet:source")
+                        .setBody(constant("Hello from the Kamelet"));
+
+                routeTemplate("keep")
+                        .from("kamelet:source")
+                        .setHeader("visited", constant(true));
+
+                routeTemplate("list")
+                        .from("kamelet:source")
+                        .setBody(constant(List.of("a", "b")));
+
+                from("direct:text").kamelet("text");
+                from("direct:keep").kamelet("keep");
+                from("direct:list").kamelet("list");
+            }
+        };
+    }
+
+    /** A body that no converter knows: a Kamelet route that keeps it gives 
the same instance. */
+    private static final class Order {
+    }
+}
diff --git 
a/components/camel-microprofile/camel-microprofile-fault-tolerance/src/test/java/org/apache/camel/component/microprofile/faulttolerance/FaultToleranceBodyTypesTest.java
 
b/components/camel-microprofile/camel-microprofile-fault-tolerance/src/test/java/org/apache/camel/component/microprofile/faulttolerance/FaultToleranceBodyTypesTest.java
new file mode 100644
index 000000000000..50b8c53cee27
--- /dev/null
+++ 
b/components/camel-microprofile/camel-microprofile-fault-tolerance/src/test/java/org/apache/camel/component/microprofile/faulttolerance/FaultToleranceBodyTypesTest.java
@@ -0,0 +1,165 @@
+/*
+ * 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.component.microprofile.faulttolerance;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.model.CircuitBreakerDefinition;
+import org.apache.camel.model.OnFallbackDefinition;
+import org.apache.camel.spi.BodyType.Carrier;
+import org.apache.camel.spi.BodyTypes;
+import org.apache.camel.spi.CircuitBreakerConstants;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import 
org.eclipse.microprofile.faulttolerance.exceptions.CircuitBreakerOpenException;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The body types of the circuitBreaker and onFallback EIPs (CAMEL-25928), 
checked at runtime with MicroProfile Fault
+ * Tolerance: the steps start with the body the circuit breaker got, and the 
body after it is what its steps end with,
+ * or what onFallback ends with when a step failed or the circuit is open. The 
EIPs implemented in core are checked by
+ * EipBodyTypesTest in camel-core.
+ */
+public class FaultToleranceBodyTypesTest extends CamelTestSupport {
+
+    /** The body the steps of circuitBreaker end with. */
+    private static final String END = "end of the steps";
+    /** The body the steps of onFallback end with. */
+    private static final String FALLBACK_END = "end of onFallback";
+    /** The body a step sets before the next step fails. */
+    private static final String FAILED = "the body the failed step left";
+
+    private final List<Object> steps = new CopyOnWriteArrayList<>();
+    private final List<Object> fallback = new CopyOnWriteArrayList<>();
+
+    @Test
+    public void theDeclarations() {
+        assertEquals("steps", 
bodyTypes(CircuitBreakerDefinition.class).eipOut().from());
+        assertEquals(Carrier.UNCHANGED, 
bodyTypes(CircuitBreakerDefinition.class).eipSteps().value()[0]);
+        assertEquals("steps", 
bodyTypes(OnFallbackDefinition.class).eipOut().from());
+        assertEquals(Carrier.UNCHANGED, 
bodyTypes(OnFallbackDefinition.class).eipSteps().value()[0]);
+    }
+
+    @Test
+    public void theStepsStartWithTheBodyAndTheBodyAfterIsWhatTheyEndWith() {
+        Order body = new Order();
+        Exchange out = send("direct:steps", body);
+        assertFalse(out.isFailed());
+        assertEquals(END, out.getMessage().getBody());
+        assertEquals(1, steps.size());
+        assertSame(body, steps.get(0));
+        assertTrue(fallback.isEmpty());
+    }
+
+    @Test
+    public void 
whenAStepFailsOnFallbackStartsWithTheBodyTheCircuitBreakerGot() {
+        Order body = new Order();
+        Exchange out = send("direct:failed", body);
+        assertFalse(out.isFailed());
+        assertEquals(FALLBACK_END, out.getMessage().getBody());
+        assertEquals(1, steps.size());
+        assertSame(body, steps.get(0));
+        // the steps run on a copy of the exchange, so onFallback gets the 
body the circuit breaker got,
+        // not the body the failed step left
+        assertEquals(1, fallback.size());
+        assertSame(body, fallback.get(0));
+        assertEquals(false, 
out.getProperty(CircuitBreakerConstants.RESPONSE_REJECTED));
+    }
+
+    @Test
+    public void 
whenTheCircuitIsOpenOnFallbackStartsWithTheBodyTheCircuitBreakerGot() {
+        // two failures open the circuit, the third call is not attempted
+        send("direct:open", new Order());
+        send("direct:open", new Order());
+
+        Order body = new Order();
+        Exchange out = send("direct:open", body);
+        assertFalse(out.isFailed());
+        assertEquals(FALLBACK_END, out.getMessage().getBody());
+        assertTrue(steps.isEmpty());
+        assertEquals(1, fallback.size());
+        assertSame(body, fallback.get(0));
+        assertEquals(true, 
out.getProperty(CircuitBreakerConstants.RESPONSE_REJECTED));
+        assertInstanceOf(CircuitBreakerOpenException.class, 
out.getProperty(Exchange.EXCEPTION_CAUGHT));
+    }
+
+    private Exchange send(String uri, Object body) {
+        steps.clear();
+        fallback.clear();
+        return template.send(uri, e -> e.getMessage().setBody(body));
+    }
+
+    private void steps(Exchange exchange) {
+        steps.add(exchange.getMessage().getBody());
+    }
+
+    private void fallback(Exchange exchange) {
+        fallback.add(exchange.getMessage().getBody());
+    }
+
+    private static BodyTypes bodyTypes(Class<?> model) {
+        BodyTypes answer = model.getAnnotation(BodyTypes.class);
+        assertNotNull(answer, model.getSimpleName() + " has no @BodyTypes");
+        return answer;
+    }
+
+    @Override
+    protected RoutesBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:steps")
+                        .circuitBreaker()
+                        .process(e -> steps(e)).setBody(constant(END))
+                        .onFallback()
+                        .process(e -> 
fallback(e)).setBody(constant(FALLBACK_END))
+                        .end();
+
+                from("direct:failed")
+                        .circuitBreaker()
+                        .process(e -> steps(e)).setBody(constant(FAILED))
+                        .throwException(new IllegalStateException("Forced"))
+                        .onFallback()
+                        .process(e -> 
fallback(e)).setBody(constant(FALLBACK_END))
+                        .end();
+
+                from("direct:open")
+                        .circuitBreaker()
+                        
.faultToleranceConfiguration().requestVolumeThreshold(2).failureRatio(50).delay(60000).end()
+                        .process(e -> steps(e)).setBody(constant(FAILED))
+                        .throwException(new IllegalStateException("Forced"))
+                        .onFallback()
+                        .process(e -> 
fallback(e)).setBody(constant(FALLBACK_END))
+                        .end();
+            }
+        };
+    }
+
+    /** A body that no converter knows: a circuit breaker that keeps it gives 
the same instance. */
+    private static final class Order {
+    }
+}
diff --git 
a/components/camel-resilience4j/src/test/java/org/apache/camel/component/resilience4j/CircuitBreakerBodyTypesTest.java
 
b/components/camel-resilience4j/src/test/java/org/apache/camel/component/resilience4j/CircuitBreakerBodyTypesTest.java
new file mode 100644
index 000000000000..0c3b0eb99972
--- /dev/null
+++ 
b/components/camel-resilience4j/src/test/java/org/apache/camel/component/resilience4j/CircuitBreakerBodyTypesTest.java
@@ -0,0 +1,170 @@
+/*
+ * 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.component.resilience4j;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import io.github.resilience4j.circuitbreaker.CallNotPermittedException;
+import io.github.resilience4j.circuitbreaker.CircuitBreaker;
+import org.apache.camel.BindToRegistry;
+import org.apache.camel.Exchange;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.model.CircuitBreakerDefinition;
+import org.apache.camel.model.OnFallbackDefinition;
+import org.apache.camel.spi.BodyType.Carrier;
+import org.apache.camel.spi.BodyTypes;
+import org.apache.camel.spi.CircuitBreakerConstants;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The body types of the circuitBreaker and onFallback EIPs (CAMEL-25928), 
checked at runtime with Resilience4j: the
+ * steps start with the body the circuit breaker got, and the body after it is 
what its steps end with, or what
+ * onFallback ends with when a step failed or the circuit is open. The EIPs 
implemented in core are checked by
+ * EipBodyTypesTest in camel-core.
+ */
+public class CircuitBreakerBodyTypesTest extends CamelTestSupport {
+
+    /** The body the steps of circuitBreaker end with. */
+    private static final String END = "end of the steps";
+    /** The body the steps of onFallback end with. */
+    private static final String FALLBACK_END = "end of onFallback";
+    /** The body a step sets before the next step fails. */
+    private static final String FAILED = "the body the failed step left";
+
+    private final List<Object> steps = new CopyOnWriteArrayList<>();
+    private final List<Object> fallback = new CopyOnWriteArrayList<>();
+
+    @BindToRegistry("openCircuitBreaker")
+    public CircuitBreaker openCircuitBreaker() {
+        return CircuitBreaker.ofDefaults("openCircuitBreaker");
+    }
+
+    @Test
+    public void theDeclarations() {
+        assertEquals("steps", 
bodyTypes(CircuitBreakerDefinition.class).eipOut().from());
+        assertEquals(Carrier.UNCHANGED, 
bodyTypes(CircuitBreakerDefinition.class).eipSteps().value()[0]);
+        assertEquals("steps", 
bodyTypes(OnFallbackDefinition.class).eipOut().from());
+        assertEquals(Carrier.UNCHANGED, 
bodyTypes(OnFallbackDefinition.class).eipSteps().value()[0]);
+    }
+
+    @Test
+    public void theStepsStartWithTheBodyAndTheBodyAfterIsWhatTheyEndWith() {
+        Order body = new Order();
+        Exchange out = send("direct:steps", body);
+        assertFalse(out.isFailed());
+        assertEquals(END, out.getMessage().getBody());
+        assertEquals(1, steps.size());
+        assertSame(body, steps.get(0));
+        assertTrue(fallback.isEmpty());
+    }
+
+    @Test
+    public void 
whenAStepFailsOnFallbackStartsWithTheBodyTheCircuitBreakerGot() {
+        Order body = new Order();
+        Exchange out = send("direct:failed", body);
+        assertFalse(out.isFailed());
+        assertEquals(FALLBACK_END, out.getMessage().getBody());
+        assertEquals(1, steps.size());
+        assertSame(body, steps.get(0));
+        // the steps run on a copy of the exchange, so onFallback gets the 
body the circuit breaker got,
+        // not the body the failed step left
+        assertEquals(1, fallback.size());
+        assertSame(body, fallback.get(0));
+        assertEquals(false, 
out.getProperty(CircuitBreakerConstants.RESPONSE_REJECTED));
+    }
+
+    @Test
+    public void 
whenTheCircuitIsOpenOnFallbackStartsWithTheBodyTheCircuitBreakerGot() {
+        CircuitBreaker breaker = 
context.getRegistry().lookupByNameAndType("openCircuitBreaker", 
CircuitBreaker.class);
+        breaker.transitionToForcedOpenState();
+
+        Order body = new Order();
+        Exchange out = send("direct:open", body);
+        assertFalse(out.isFailed());
+        assertEquals(FALLBACK_END, out.getMessage().getBody());
+        // the call is not attempted
+        assertTrue(steps.isEmpty());
+        assertEquals(1, fallback.size());
+        assertSame(body, fallback.get(0));
+        assertEquals(true, 
out.getProperty(CircuitBreakerConstants.RESPONSE_REJECTED));
+        assertInstanceOf(CallNotPermittedException.class, 
out.getProperty(Exchange.EXCEPTION_CAUGHT));
+    }
+
+    private Exchange send(String uri, Object body) {
+        steps.clear();
+        fallback.clear();
+        return template.send(uri, e -> e.getMessage().setBody(body));
+    }
+
+    private void steps(Exchange exchange) {
+        steps.add(exchange.getMessage().getBody());
+    }
+
+    private void fallback(Exchange exchange) {
+        fallback.add(exchange.getMessage().getBody());
+    }
+
+    private static BodyTypes bodyTypes(Class<?> model) {
+        BodyTypes answer = model.getAnnotation(BodyTypes.class);
+        assertNotNull(answer, model.getSimpleName() + " has no @BodyTypes");
+        return answer;
+    }
+
+    @Override
+    protected RoutesBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:steps")
+                        .circuitBreaker()
+                        .process(e -> steps(e)).setBody(constant(END))
+                        .onFallback()
+                        .process(e -> 
fallback(e)).setBody(constant(FALLBACK_END))
+                        .end();
+
+                from("direct:failed")
+                        .circuitBreaker()
+                        .process(e -> steps(e)).setBody(constant(FAILED))
+                        .throwException(new IllegalStateException("Forced"))
+                        .onFallback()
+                        .process(e -> 
fallback(e)).setBody(constant(FALLBACK_END))
+                        .end();
+
+                from("direct:open")
+                        
.circuitBreaker().resilience4jConfiguration().circuitBreaker("openCircuitBreaker").end()
+                        .process(e -> steps(e)).setBody(constant(END))
+                        .onFallback()
+                        .process(e -> 
fallback(e)).setBody(constant(FALLBACK_END))
+                        .end();
+            }
+        };
+    }
+
+    /** A body that no converter knows: a circuit breaker that keeps it gives 
the same instance. */
+    private static final class Order {
+    }
+}

Reply via email to