From b3f68b3bb73e99fa0a077047ece041d7c9af05dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Diego=20L=C3=B3pez?= Date: Wed, 24 Jun 2026 12:57:15 -0400 Subject: [PATCH 01/11] =?UTF-8?q?ci:=20remove=20duplicate=20push=20trigger?= =?UTF-8?q?=20=E2=80=94=20deploy-snapshot.yml=20covers=20post-merge=20vali?= =?UTF-8?q?dation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ci.yml and deploy-snapshot.yml both triggered on push to main and both called test.yml — running the full Java matrix (17/21/25 × 22 modules) twice in parallel for every merge. Removes ci.yml's push trigger so: - ci.yml validates PRs to main and develop (gate before merge) - deploy-snapshot.yml validates the merged state on main + publishes snapshot Same test coverage, half the CI minutes per merge. Direct pushes to main (bypassing PR) are still test-covered via deploy-snapshot.yml's preflight -> test chain. --- .github/workflows/ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de02f5a..ab4dc33 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,8 +3,6 @@ name: CI on: pull_request: branches: [main, develop] - push: - branches: [main] concurrency: group: ci-${{ github.ref }} From aa037711aa19758cc6fd265dfec78b6c3bfeb5a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Diego=20L=C3=B3pez?= Date: Wed, 24 Jun 2026 13:47:38 -0400 Subject: [PATCH 02/11] docs(branding): add Ark logo and integrate into README header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds three SVG variants under assets/logo/: - ark-icon.svg: 128×128 square tile, ark silhouette on teal gradient (peaked roof + trapezoidal hull + waves) — for favicons and avatars - ark-logo.svg: 420×128 icon + wordmark for light theme (slate-900) - ark-logo-dark.svg: same with near-white wordmark for dark theme README header replaces the plain '# Ark 🛳' with a centered

wrapping a element that swaps between light and dark logo variants based on the viewer's color-scheme preference. Tagline and badges are now also centered for a cohesive header. --- README.md | 25 +++++++++++++++-------- assets/logo/ark-icon.svg | 29 ++++++++++++++++++++++++++ assets/logo/ark-logo-dark.svg | 38 +++++++++++++++++++++++++++++++++++ assets/logo/ark-logo.svg | 38 +++++++++++++++++++++++++++++++++++ 4 files changed, 122 insertions(+), 8 deletions(-) create mode 100644 assets/logo/ark-icon.svg create mode 100644 assets/logo/ark-logo-dark.svg create mode 100644 assets/logo/ark-logo.svg diff --git a/README.md b/README.md index 08dcf1e..f472527 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,20 @@ -# Ark 🛳 - -[![Maven Central](https://img.shields.io/maven-central/v/xyz.juandiii/ark-core?label=Maven%20Central)](https://central.sonatype.com/namespace/xyz.juandiii) -[![CI](https://github.com/juandiii/ark/actions/workflows/ci.yml/badge.svg)](https://github.com/juandiii/ark/actions/workflows/ci.yml) -[![Java 17+](https://img.shields.io/badge/Java-17%2B-orange.svg)](https://adoptium.net/) -[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE) - -**A modular HTTP client toolkit for Java 17+ with fluent and declarative APIs, pluggable transports, composable decorators, and support for sync, async, and reactive applications.** +

+ + + Ark + +

+ +

+ A modular HTTP client toolkit for Java 17+ with fluent and declarative APIs, pluggable transports, composable decorators, and support for sync, async, and reactive applications. +

+ +

+ Maven Central + CI + Java 17+ + License: Apache 2.0 +

--- diff --git a/assets/logo/ark-icon.svg b/assets/logo/ark-icon.svg new file mode 100644 index 0000000..fc94da1 --- /dev/null +++ b/assets/logo/ark-icon.svg @@ -0,0 +1,29 @@ + + Ark + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/logo/ark-logo-dark.svg b/assets/logo/ark-logo-dark.svg new file mode 100644 index 0000000..78a8a27 --- /dev/null +++ b/assets/logo/ark-logo-dark.svg @@ -0,0 +1,38 @@ + + Ark + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ark + diff --git a/assets/logo/ark-logo.svg b/assets/logo/ark-logo.svg new file mode 100644 index 0000000..81f1637 --- /dev/null +++ b/assets/logo/ark-logo.svg @@ -0,0 +1,38 @@ + + Ark + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ark + From 8ea817034abefd84898f1769172f676ff102316c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Diego=20L=C3=B3pez?= Date: Wed, 24 Jun 2026 13:55:17 -0400 Subject: [PATCH 03/11] fix(branding): tighten ark-logo viewBox to remove right-side whitespace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 420×128 viewBox left ~150 units of empty space to the right of the wordmark — when centered in the README, the bounding box was centered but the visible content appeared off-center to the left. Tightens viewBox to 280×128 so it matches the actual content bounds: icon ends at x=120, wordmark ends near x=265. Centering now reflects the visible content. --- assets/logo/ark-logo-dark.svg | 2 +- assets/logo/ark-logo.svg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/logo/ark-logo-dark.svg b/assets/logo/ark-logo-dark.svg index 78a8a27..af966ea 100644 --- a/assets/logo/ark-logo-dark.svg +++ b/assets/logo/ark-logo-dark.svg @@ -1,4 +1,4 @@ - + Ark diff --git a/assets/logo/ark-logo.svg b/assets/logo/ark-logo.svg index 81f1637..20e4c65 100644 --- a/assets/logo/ark-logo.svg +++ b/assets/logo/ark-logo.svg @@ -1,4 +1,4 @@ - + Ark From fa7d8f61b0b5066c03ba698f332f65e9ffad117e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Diego=20L=C3=B3pez?= Date: Wed, 24 Jun 2026 15:48:14 -0400 Subject: [PATCH 04/11] =?UTF-8?q?docs(branding):=20refine=20ark=20logo=20?= =?UTF-8?q?=E2=80=94=20keep=20side=20waves=20and=20original=20ark=20positi?= =?UTF-8?q?on?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After iterating on wave placement and ark vertical centering, reverts to the original design with two short side waves at y=96 and the ark sitting at y=74-102. Cleaner visual composition — waves visible to both sides of the hull, ark anchored in the lower portion of the tile. --- assets/logo/ark-icon.svg | 44 ++++++++++++------------- assets/logo/ark-logo-dark.svg | 62 +++++++++++++++++------------------ assets/logo/ark-logo.svg | 62 +++++++++++++++++------------------ 3 files changed, 83 insertions(+), 85 deletions(-) diff --git a/assets/logo/ark-icon.svg b/assets/logo/ark-icon.svg index fc94da1..abc0f05 100644 --- a/assets/logo/ark-icon.svg +++ b/assets/logo/ark-icon.svg @@ -1,29 +1,27 @@ - Ark - - - - - - + Ark + + + + + + - - + + - - - - - + + + + + - - - - - - - + + + + + - - + + diff --git a/assets/logo/ark-logo-dark.svg b/assets/logo/ark-logo-dark.svg index af966ea..04f7c51 100644 --- a/assets/logo/ark-logo-dark.svg +++ b/assets/logo/ark-logo-dark.svg @@ -1,38 +1,38 @@ - Ark - - - - - - + Ark + + + + + + - - - - + + + + - - - - - + + + + + - - - - - + + + + + - - - + + + - - ark + + ark diff --git a/assets/logo/ark-logo.svg b/assets/logo/ark-logo.svg index 20e4c65..04f7c51 100644 --- a/assets/logo/ark-logo.svg +++ b/assets/logo/ark-logo.svg @@ -1,38 +1,38 @@ - Ark - - - - - - + Ark + + + + + + - - - - + + + + - - - - - + + + + + - - - - - + + + + + - - - + + + - - ark + + ark From 1dfb6070a8f586b24e9967ec90b65a10dca62f96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Diego=20L=C3=B3pez?= Date: Wed, 24 Jun 2026 16:58:05 -0400 Subject: [PATCH 05/11] fix(branding): restore ark-logo.svg light theme colors (gradient id + wordmark fill) --- assets/logo/ark-logo.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/logo/ark-logo.svg b/assets/logo/ark-logo.svg index 04f7c51..81bba74 100644 --- a/assets/logo/ark-logo.svg +++ b/assets/logo/ark-logo.svg @@ -1,7 +1,7 @@ Ark - + @@ -10,7 +10,7 @@ - + @@ -33,6 +33,6 @@ font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, sans-serif" font-size="80" font-weight="700" - fill="#f8fafc" + fill="#0f172a" letter-spacing="-3">ark From 698828268f8e6297df79493e5a5cfe1009d614d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Diego=20L=C3=B3pez?= Date: Mon, 29 Jun 2026 19:15:42 -0400 Subject: [PATCH 06/11] docs(readme): rewrite to centered pitch + capabilities + comparison + architecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructures README.md following a flagship-OSS layout: - Centered logo (picture element, light/dark variants) + tagline + badges - 'What is Ark?' with emoji-prefixed core capabilities - Quick Start (declarative + fluent), with explicit Ark/ArkClient.Builder injection - Frameworks collapsibles (Spring sync, Spring WebFlux, Quarkus, standalone) - Execution Models table with the 5 supported return-type families - 'How it compares' table vs Apache HC5, OkHttp, Spring RestClient, OpenFeign - Architecture mermaid diagram (API surface -> core pipeline -> transport) - Documentation index, Roadmap with delivered vs upcoming items, License Notable accuracy fix in the comparison table: Spring RestClient is marked as supporting declarative interfaces (via @HttpExchange) — present since Spring Framework 6 / Boot 3.x. --- README.md | 581 +++++++++++++++++++----------------------------------- 1 file changed, 203 insertions(+), 378 deletions(-) diff --git a/README.md b/README.md index f472527..f8e215f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +
+

@@ -5,482 +7,305 @@

-

- A modular HTTP client toolkit for Java 17+ with fluent and declarative APIs, pluggable transports, composable decorators, and support for sync, async, and reactive applications. -

- -

- Maven Central - CI - Java 17+ - License: Apache 2.0 -

+### Modular HTTP client toolkit for Java 17+ ---- +One client model that survives framework changes, transport changes, and execution-model changes. +Write **fluent** or **declarative** code; pick **JDK / Apache / Reactor Netty / Vert.x** under the +hood; run **sync / async / reactive**; host on **Spring Boot**, **Quarkus**, or a plain `main()`. +Compose **retry**, **metrics**, or your own behavior through `transport.with(...)` decorators. -## The Problem +[![Maven Central](https://img.shields.io/maven-central/v/xyz.juandiii/ark-core?label=Maven%20Central)](https://central.sonatype.com/namespace/xyz.juandiii) +[![CI](https://github.com/juandiii/ark/actions/workflows/ci.yml/badge.svg)](https://github.com/juandiii/ark/actions/workflows/ci.yml) +[![Java 17+](https://img.shields.io/badge/Java-17%2B-orange.svg)](https://adoptium.net/) +[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE) -Java has no shortage of HTTP clients. But each one forces a trade-off: +[**Quick Start**](#quick-start) • [**Frameworks**](#frameworks) • [**Execution Models**](#execution-models) • [**Architecture**](#architecture) • [**Documentation**](#documentation) -- **JDK HttpClient** - low-level, no serialization, no interceptors, no declarative API -- **Spring WebClient / RestClient** - optimized for Spring applications and programming models -- **Quarkus REST Client** - tightly aligned with Quarkus and JAX-RS-style declarative clients -- **OkHttp / Apache HttpClient** - transport only, you build everything else yourself -- **Feign** - declarative only, no fluent API, limited reactive support +
-Ark gives you one client model that survives framework changes, transport changes, and execution-model changes. +
+Named after the ark - a vessel that carries safely across distance. +
-## The Solution +
-Ark separates the concerns that other clients bundle together: +## What is Ark? -| Concern | Ark's approach | -|---------|---------------| -| **How to build requests** | Fluent API or declarative interfaces - your choice | -| **How to send them** | Pluggable transports - JDK, Reactor Netty, Vert.x, Apache | -| **How to serialize** | Pluggable serializers - Jackson, JSON-B, or your own | -| **How to execute** | Sync, async, Reactor, Mutiny, Vert.x Future - same API | -| **How to compose behavior** | Decorators chain via `transport.with(...)` - retry, metrics, your own | -| **Where to run** | Spring Boot, Quarkus, or standalone - same code | +Ark is an HTTP client toolkit for Java that separates the concerns other clients bundle: how you +**write** requests (fluent vs declarative), how they're **sent** (which transport), how they're +**serialized** (which JSON library), how they **execute** (sync / async / reactive), and where +they **run** (Spring / Quarkus / standalone). Each axis is pluggable - the rest stays the same. -One mental model. Any stack. No lock-in. +**Core capabilities:** -## Why Ark? - -- **One client model across stacks** - use Ark in Spring, Quarkus, and plain Java -- **Fluent when you want control** - explicit request composition with full access to headers, params, and body -- **Declarative when you want contracts** - `@RegisterArkClient` with Spring `@HttpExchange` or JAX-RS annotations -- **Transport-agnostic** - plug in JDK, Reactor Netty, Vert.x, or Apache HttpClient -- **Execution-model aware** - sync, async, Reactor, Mutiny, and Vert.x Future -- **Generic `Transport` contract** - every execution model implements the same interface parameterized on its return wrapper -- **Composable decorators** - `transport.with(Retry.of(policy, ops))` chain works across all 5 models; bring your own (`Metrics`, `Cache`, `CircuitBreaker`) -- **Production-ready features** - TLS, retry, redacted logging, typed exceptions, per-client config -- **Async stacktraces include the caller site** - no more "lost" call frames in `CompletableFuture` failures -- **Native-image friendly** - designed to work well in GraalVM-based deployments +- 🧩 **Fluent + declarative** - the `Ark` builder API _or_ `@RegisterArkClient` interfaces with `@HttpExchange` / JAX-RS annotations +- 🚢 **Pluggable transports** - JDK HttpClient, Apache HC5, Reactor Netty, Vert.x WebClient - swap without changing call sites +- 🔌 **Composable decorators** - `transport.with(Retry.of(...))` chains retry, your metrics, your tracing - same pattern everywhere +- ⚡ **Five execution models** - sync, async (`CompletableFuture`), Reactor (`Mono` / `Flux`), Mutiny (`Uni` / `Multi`), Vert.x (`Future`) - one API shape +- 🍃 **Any host** - Spring Boot MVC, Spring Boot WebFlux, Quarkus (JVM + native), or standalone - one client model +- 🛡️ **Permissive error handling** - `.noThrow()` per request _or_ `ark.client..throw-on-error=false` - inspect 4xx/5xx without exceptions +- 🔍 **Raw response access** - `.raw()` on every `*ClientResponse` _or_ declare `RawResponse` as a proxy return type - bypass deserialization when needed +- ⚙️ **GraalVM native** - reflection / proxy hints emitted automatically for both Spring Boot AOT and Quarkus build-time +- 📊 **Verified compat** - upstream Spring Boot / Quarkus patches tested weekly via CI matrix --- -## Client Styles - -Ark supports multiple ways to define HTTP clients. +## Quick Start -### Fluent API +**1. Add Ark** - import the BOM and a host starter (Spring shown; [other frameworks](#frameworks) below): -Use the fluent API when you want full control over request composition. - -```java -import org.springframework.http.MediaType; // or jakarta.ws.rs.core.MediaType -import xyz.juandiii.ark.core.Ark; -import xyz.juandiii.ark.core.ArkClient; -import xyz.juandiii.ark.jackson.JacksonSerializer; -import xyz.juandiii.ark.transport.jdk.ArkJdkSyncTransport; -import java.net.http.HttpClient; - -Ark client = ArkClient.builder() - .serializer(new JacksonSerializer(new ObjectMapper())) - .transport(new ArkJdkSyncTransport(HttpClient.newBuilder().build())) - .baseUrl("https://api.example.com") - .build(); +```xml + + + + xyz.juandiii + ark-bom + ${ark.version} + pom + import + + + -User user = client.get("/users/1") - .accept(MediaType.APPLICATION_JSON_VALUE) - .retrieve() - .body(User.class); + + xyz.juandiii + ark-spring-boot-starter + ``` -### Declarative Clients - -Define an interface with `@RegisterArkClient` and inject it directly: +**2. Write a declarative client:** ```java -@RegisterArkClient(baseUrl = "${api.users.url}") +@RegisterArkClient(configKey = "users-api") @HttpExchange("/users") public interface UserApi { @GetExchange("/{id}") User getUser(@PathVariable String id); + + @PostExchange + User createUser(@RequestBody User user); } ``` -```java -// Spring -public UserController(UserApi userApi) { ... } - -// Quarkus -@Inject UserApi userApi; +```properties +ark.client.users-api.base-url=https://api.example.com +ark.client.users-api.connect-timeout=5 ``` -Supports Spring `@HttpExchange` and JAX-RS `@Path`/`@GET`/`@POST` annotations. - -### Spring Async — same `@RegisterArkClient` - -If any method on the interface returns `CompletableFuture`, the Spring starter auto-wires an `AsyncArkClient` proxy instead of `ArkClient`. Zero extra configuration: +**3. Inject and call it:** ```java -@RegisterArkClient(configKey = "users-api") -@HttpExchange("/users") -public interface UserApi { +@Service +public class UserService { + private final UserApi api; + UserService(UserApi api) { this.api = api; } - @GetExchange("/{id}") - CompletableFuture getUser(@PathVariable String id); // ← async + public User find(String id) { return api.getUser(id); } } ``` -> **IDE hint**: if IntelliJ reports `Could not autowire. No beans of 'UserApi' type found.`, the bean **does** exist at runtime — Ark registers it dynamically. Add `@org.springframework.stereotype.Component` on the interface alongside `@RegisterArkClient` to silence the warning. Spring's default scan skips interfaces, so no double-registration. See [docs/spring-boot.md → IDE autowiring hint](docs/spring-boot.md#ide-autowiring-hint). - -### JAX-RS Example +Or use the **fluent** API directly — inject `ArkClient.Builder` (auto-configured by the starter): ```java -@RegisterArkClient(baseUrl = "${api.users.url}") -@Path("/users") -@Produces("application/json") -public interface UserApi { - - @GET - @Path("/{id}") - User getUser(@PathParam("id") String id); +@Service +public class UserService { + private final Ark client; + + UserService(ArkClient.Builder builder) { + this.client = builder.baseUrl("https://api.example.com").build(); + } + + public User find(String id) { + return client.get("/users/" + id) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .body(User.class); + } } ``` -```java -// Quarkus -@Inject UserApi userApi; -``` - -### @RegisterArkClient Attributes - -| Attribute | Default | Description | -|-----------|---------|-------------| -| `configKey` | `""` | Key for per-client config in `application.properties` | -| `baseUrl` | `""` | Base URL, supports `${property}` placeholders | -| `httpVersion` | `HTTP_2` | HTTP/1.1 or HTTP/2 | -| `connectTimeout` | `10` | Connection timeout (seconds) | -| `readTimeout` | `30` | Read timeout (seconds) | -| `interceptors` | `{}` | Interceptor classes (auto-detects Request/Response) | - ---- - -## Composable Decorators - -Every `Transport` exposes a `.with(...)` method that composes decorators. Built-in decorator: `Retry`. Custom decorators (metrics, caching, circuit breaker) plug in the same way. - -```java -import xyz.juandiii.ark.core.http.decorator.Retry; -import xyz.juandiii.ark.core.http.decorator.SyncRetryOps; - -Transport resilient = new ArkJdkSyncTransport(HttpClient.newBuilder().build()) - .with(Retry.of(retryPolicy, new SyncRetryOps())) - // .with(MyMetrics.of(registry)) - // .with(MyCache.of(store)); - -Ark client = ArkClient.builder() - .serializer(serializer) - .transport(resilient) - .baseUrl("https://api.example.com") - .build(); -``` - -The chain composes **outside-in** — the last `.with(...)` is the outermost wrapper. `RetryOps` strategies exist per execution model: `SyncRetryOps`, `AsyncRetryOps`, `ReactorRetryOps`, `MutinyRetryOps`, `VertxRetryOps`. See [Retry & Backoff](docs/retry.md) for ordering rules (e.g., `Metrics` outside `Retry` measures total wall-clock; inside, per-attempt). +The same model runs on every supported host - only the registration differs. --- -## Features - -- Java 17+ -- Fluent HTTP API -- Declarative HTTP clients with **Spring `@HttpExchange`** or **JAX-RS `@Path`/`@GET`** -- `@RegisterArkClient` for zero-boilerplate auto-registration and injection (sync + async) -- Generic `Transport` contract unified across all 5 execution models -- Composable `.with(...)` decorator chain (built-in `Retry`; bring your own) -- Pluggable transports (JDK, Reactor Netty, Vert.x, Apache HttpClient 5) -- Pluggable serializers (Jackson, Jackson Classic, JSON-B) -- Dedicated sync, async, Reactor, Mutiny, and Vert.x APIs -- Type-safe per-client configuration (`ArkProperties` / `@ConfigMapping`) -- Per-client interceptors and default headers via config -- Retry with exponential backoff and jitter (`Retry` decorator) -- Async stacktraces preserve the caller site (suppressed exception, no lost frames) -- TLS/SSL support (Spring SSL Bundles, Quarkus TLS Registry) -- Trust-all SSL for development (with runtime warning) -- Request/response logging with sensitive-header and credential-body redaction (`NONE`, `BASIC`, `HEADERS`, `BODY`) -- Typed exception hierarchy (400-504 mapped to specific exceptions) -- **Permissive error handling** — opt out of throw-on-4xx/5xx per request - (`.noThrow()`) or at the client level (`throwOnError(false)`). Useful - when 4xx is business semantics (e.g. 404 = not found, not an error). -- **Raw response access** — `.raw()` on every `*ClientResponse`, or declare - `RawResponse` as a proxy method return type. Bypasses deserialization - and auto-disables throw-on-error — useful for inspecting error bodies - or non-JSON responses. -- Per-request timeout support -- HTTP/2 by default -- Spring Boot (sync + async + WebFlux) and Quarkus integration -- GraalVM native image support -- Easy to test and mock - ---- - -## Execution Models - -Ark provides dedicated entry points for different execution models while preserving a consistent client experience. - -| Model | Client | Return Type | -|-------|--------|-------------| -| Sync | `ArkClient` | `T` | -| Async | `AsyncArkClient` | `CompletableFuture` | -| Reactor | `ReactorArkClient` | `Mono` | -| Mutiny | `MutinyArkClient` | `Uni` | -| Vert.x | `VertxArkClient` | `Future` | - -Same fluent API - only the return type changes: - -```java -User user = client - .get("/users/1") - .retrieve() - .body(User.class); - -CompletableFuture cf = asyncClient - .get("/users/1") - .retrieve() - .body(User.class); - -Mono mono = reactorClient - .get("/users/1") - .retrieve() - .body(User.class); - -Uni uni = mutinyClient - .get("/users/1") - .retrieve() - .body(User.class); - -Future future = vertxClient - .get("/users/1") - .retrieve() - .body(User.class); -``` - ---- - -## Installation - -Import the BOM first: - -```xml - - - - xyz.juandiii - ark-bom - 1.0.7 - pom - import - - - -``` - -Then choose the modules you need. +## Frameworks -### Core + Jackson + JDK transport +All coordinates under `groupId` **`xyz.juandiii`**, versioned by the [BOM](#quick-start). Pick a host: -```xml - - - xyz.juandiii - ark-core - - - xyz.juandiii - ark-jackson - - - xyz.juandiii - ark-transport-jdk - - -``` - -### Optional modules - -For async support: +
+Spring Boot (sync / MVC) ```xml - xyz.juandiii - ark-async + xyz.juandiii + ark-spring-boot-starter ``` -For Reactor support: +Annotate interfaces with `@RegisterArkClient` + Spring's `@HttpExchange` family. The starter +auto-configures `Ark`, `AsyncArk`, and the proxy factory; AOT hints are emitted for native image. -```xml - - xyz.juandiii - ark-reactor - - - xyz.juandiii - ark-transport-reactor - -``` +
-For Mutiny support: +
+Spring Boot (reactive / WebFlux) ```xml - xyz.juandiii - ark-mutiny - - - xyz.juandiii - ark-transport-vertx-mutiny + xyz.juandiii + ark-spring-boot-starter-webflux ``` -For Vert.x `Future` support: +Proxy methods return `Mono` / `Flux`; transport is Reactor Netty by default. Same +`@RegisterArkClient` interfaces work — just declare reactive return types. -```xml - - xyz.juandiii - ark-vertx - - - xyz.juandiii - ark-transport-vertx - -``` +
-For Spring Boot: +
+Quarkus (JVM + native) ```xml - xyz.juandiii - ark-spring-boot-starter + xyz.juandiii + ark-quarkus-jackson ``` -For Spring WebFlux: +Annotate interfaces with `@RegisterArkClient` + JAX-RS (`@Path` / `@GET` / `@POST`) — or use +Spring's `@HttpExchange` if you prefer. Build-time reflection + proxy hints emitted for native. -```xml - - xyz.juandiii - ark-spring-boot-starter-webflux - -``` +
-For Quarkus (Jackson): +
+Plain main() -```xml - - xyz.juandiii - ark-quarkus-jackson - +```java +Ark client = ArkClient.builder() + .serializer(new JacksonSerializer(new ObjectMapper())) + .transport(new ArkJdkSyncTransport(HttpClient.newHttpClient())) + .baseUrl("https://api.example.com") + .build(); ``` -Auto-configures `JsonSerializer` (Jackson 2.x), `ArkClient.Builder` (sync), and `MutinyArkClient.Builder` (reactive) as CDI beans. - ---- +`ark-core` has zero framework dependencies — assemble it yourself. -## Transport Model - -Ark uses a **bridge pattern**. - -The transport layer is a thin adapter around an already configured HTTP client. Ark does not own connection pools, low-level HTTP tuning, or TLS setup. Those concerns stay in the underlying transport. - -All transports implement a single generic contract `Transport` where `R` is the return-type wrapper for the execution model. Decorators compose via `transport.with(...)`. - -Built-in transports include: - -- JDK `HttpClient` — split into `ArkJdkSyncTransport` (sync) and `ArkJdkAsyncTransport` (CompletableFuture); both can share the same underlying `HttpClient` for a shared connection pool -- Reactor Netty (`ArkReactorNettyTransport`) -- Vert.x Web Client (`ArkVertxFutureTransport`) -- Vert.x Mutiny Web Client (`ArkVertxMutinyTransport`) -- Apache HttpClient 5 (`ArkApacheTransport`) - -You can also provide your own transport implementation or custom decorator — see [Transport Model](docs/transports.md). +
--- -## Logging - -Ark logs requests and responses via `LoggingInterceptor` (sensitive headers and known credential body keys are redacted). Enable it per-client with `ark.logging.level=BASIC|HEADERS|BODY` (Spring / Quarkus) or programmatically via `LoggingInterceptor.apply(builder, Level.HEADERS)`. - -For raw wire-level transport debugging, enable the underlying client's own logger: - -- **JDK HttpClient**: `-Djdk.httpclient.HttpClient.log=all` -- **Apache HttpClient 5**: set `org.apache.hc.client5.http` to DEBUG -- **Reactor Netty**: set `reactor.netty.http.client.HttpClient` to DEBUG -- **Vert.x WebClient**: set `io.vertx.core.http.impl` to DEBUG - ---- - -## Security - -Found a vulnerability? Please follow the disclosure process in [SECURITY.md](SECURITY.md) — do not open a public issue. +## Execution Models -### TLS +The **same** `@RegisterArkClient` interface works across all five execution models. Pick by return type: -Ark validates TLS certificates by default. To use a custom truststore (self-signed CA, mutual TLS), configure your SSL bundle (Spring) or TLS configuration (Quarkus) and reference it via `ark.client..tls-configuration-name`. +| Model | Module | Return type | Builder | +|---|---|---|---| +| **Sync** | `ark-core` | `T`, `ArkResponse`, `RawResponse` | `ArkClient.builder()` | +| **Async** | `ark-async` | `CompletableFuture` | `AsyncArkClient.builder()` | +| **Reactor** | `ark-reactor` | `Mono` / `Flux` | `ReactorArkClient.builder()` | +| **Mutiny** | `ark-mutiny` | `Uni` / `Multi` | `MutinyArkClient.builder()` | +| **Vert.x** | `ark-vertx` | `io.vertx.core.Future` | `VertxArkClient.builder()` | -> ⚠️ **`trust-all: true` disables ALL certificate validation.** Use only in local development against ephemeral environments. Setting `ark.client..trust-all=true` in production exposes your application to man-in-the-middle attacks. Ark logs a runtime WARNING when trust-all is active so accidental production use is visible. +```java +@RegisterArkClient(configKey = "users-api") +public interface UserApi { + @GetExchange("/{id}") User getUserSync(String id); + @GetExchange("/{id}") CompletableFuture getUserAsync(String id); + @GetExchange("/{id}") Mono getUserReactive(String id); + @GetExchange("/{id}") RawResponse getUserRaw(String id); // bypass deserialization +} +``` --- -## Documentation +## How it compares -- [CHANGELOG](CHANGELOG.md) - release notes and migration guidance -- [Compatibility Matrix](docs/compatibility.md) - supported Spring Boot, Quarkus, and Java versions -- [Getting Started](docs/getting-started.md) -- [Sync Client](docs/sync.md) -- [Async Client](docs/async.md) -- [Reactor Client](docs/reactor.md) -- [Mutiny Client](docs/mutiny.md) -- [Vert.x Client](docs/vertx.md) -- [Transport Model](docs/transports.md) - `Transport` contract, built-in transports, custom transports, `.with(...)` decorator chain -- [Retry & Backoff](docs/retry.md) - `Retry` + per-model `RetryOps` strategies; native operator alternatives -- [Serialization](docs/serialization.md) - Jackson, JSON-B, custom -- [Logging](docs/logging.md) - `LoggingInterceptor` with redaction, wire-level escape hatches -- [Multipart Upload](docs/multipart.md) - file upload with binary fidelity -- [Error Handling](docs/error-handling.md) - typed exception hierarchy -- [Declarative Spring Clients](docs/declarative-spring.md) -- [Declarative JAX-RS Clients](docs/declarative-jaxrs.md) -- [Spring Boot Integration](docs/spring-boot.md) - sync + async + WebFlux, config, TLS, IDE autowiring hint -- [Quarkus Integration](docs/quarkus.md) -- [Quarkus Jackson Extension](docs/quarkus-jackson.md) -- [Testing](docs/testing.md) -- [Design Principles](docs/design.md) +| | Ark | Apache HC5 | OkHttp | Spring RestClient | OpenFeign | +|---|:---:|:---:|:---:|:---:|:---:| +| Fluent API | ✅ | ⚠️ (Fluent ext) | ✅ | ✅ | ❌ | +| Declarative interfaces | ✅ | ❌ | ❌ | ✅ (via `@HttpExchange`) | ✅ | +| **Same interface across sync + async + reactive** | ✅ | ❌ | ❌ | ❌ (RestClient vs WebClient) | partial | +| Pluggable transports without code changes | ✅ | ❌ | ❌ | ⚠️ (`ClientHttpRequestFactory`) | ⚠️ | +| Decorator chain (`.with(...)`) | ✅ | ❌ | partial (interceptors) | ❌ | partial | +| Spring **+** Quarkus **+** standalone host | ✅ | standalone | standalone | Spring only | Spring only | +| Permissive error handling | ✅ | ✅ (manual) | ✅ (default) | partial (`onStatus`) | partial (`ErrorDecoder`) | +| GraalVM native | ✅ | ⚠️ | partial | ✅ | partial | --- -## Design Principles +## Architecture + +`ark-core` is plain Java with zero framework dependency. Hosts (Spring, Quarkus) plug in through +thin SPIs: `JsonSerializer`, `HttpTransport`, `RequestInterceptor`. Decorators stack via +`transport.with(...)` regardless of execution model. + +```mermaid +flowchart LR + code([Your code]):::io + + subgraph API["API surface"] + direction TB + FLUENT["Fluent builder
client.get().retrieve()"]:::surface + DECL["@RegisterArkClient
interface-driven"]:::surface + end + + subgraph CORE["ark-core pipeline — same model across hosts"] + direction LR + BUILD["Build & encode"]:::core + DECO["Decorator chain
retry, your own"]:::core + VAL["Validate & decode
or skip via .noThrow()"]:::core + end + + transport([Transport · JDK · Apache · Netty · Vert.x]):::io + + code --> FLUENT + code --> DECL + FLUENT --> BUILD + DECL --> BUILD + BUILD --> DECO + DECO <-->|HTTP| transport + DECO --> VAL --> code + + classDef surface fill:#e8f0fe,stroke:#4285f4,color:#202124; + classDef core fill:#e6f4ea,stroke:#34a853,color:#202124; + classDef io fill:#f1f3f4,stroke:#9aa0a6,color:#202124; +``` -- Keep transport explicit -- Keep serialization replaceable -- Support fluent and declarative styles -- Keep execution models separate at the API surface, unified at the transport contract -- Stay framework-friendly -- Prefer composition over lock-in (`transport.with(...)`) +The full nine-axis breakdown — 19 modules under `core/`, `execution-models/`, `transports/`, +`serializers/`, `proxies/`, `starters/`, `extensions/` — is in [`docs/design.md`](docs/design.md). --- -## Build +## Documentation -```bash -mvn clean install -mvn clean install -DskipTests -mvn test -``` +- [Getting Started](docs/getting-started.md) - fluent + declarative basics +- [Sync](docs/sync.md) / [Async](docs/async.md) / [Reactor](docs/reactor.md) / [Mutiny](docs/mutiny.md) - per-execution-model guides +- [Transport Model](docs/transports.md) - bridge pattern + decorator chain +- [Spring Boot Integration](docs/spring-boot.md) - starter + properties + AOT +- [Declarative Spring](docs/declarative-spring.md) - `@RegisterArkClient` + `@HttpExchange` +- [Quarkus](docs/quarkus-jackson.md) - extension + native image +- [Retry & Backoff](docs/retry.md) - decorator-based retry with per-model strategies +- [Logging](docs/logging.md) - `LoggingInterceptor` levels and redaction +- [Compatibility Matrix](docs/compatibility.md) - supported Spring Boot / Quarkus / Java versions +- [`docs/design.md`](docs/design.md) - architecture, SPIs, and the module layout --- -## Contributing - -Contributions are welcome! +## Roadmap -Please read [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on commit conventions, PR labels, and the release process. +- [x] Modular layout (19 modules grouped under semantic subdirectories) +- [x] Composable transport decorators (`transport.with(Retry.of(...))`) per execution model +- [x] Permissive error handling — per-request `.noThrow()` + client-level `throw-on-error` property +- [x] Raw response access — `.raw()` fluent + `RawResponse` as proxy return type +- [x] Weekly upstream compat sweep (Spring Boot / Quarkus latest patches via CI matrix) +- [ ] `RawResponse` return type for Vert.x proxies (handler stub pending) +- [ ] Observability decorators — OpenTelemetry tracing, Micrometer metrics +- [ ] Spring Cloud `@RefreshScope` support for hot-reload of `@RegisterArkClient` configs +- [ ] Tests for `ark-spring-boot-starter*` and `ark-quarkus-jackson` (plan 012) --- ## License -Apache 2.0 +Apache 2.0. See [LICENSE](LICENSE). From e3d686c836e41909055d7b5718799b71f35cd938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Diego=20L=C3=B3pez?= Date: Tue, 30 Jun 2026 21:48:41 -0400 Subject: [PATCH 07/11] =?UTF-8?q?refactor(quarkus-jackson):=20remove=20Ver?= =?UTF-8?q?tx=20code=20from=20slim=20extension=20=E2=80=94=20moving=20to?= =?UTF-8?q?=20new=20add-on?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Vert.x Mutiny transport + Vertx TLS resolver + Mutiny CDI producers are moved out of ark-quarkus-jackson into a new add-on module (ark-quarkus-jackson-vertx, added in the next commit). The slim extension now depends on JDK transport only — no quarkus-vertx, no ark-mutiny, no ark-transport-vertx-mutiny. The recorder switches from ArkJaxRsProxy.create to ArkProxy.create so proxy-provider/execution-model resolution stays Class.forName-based and does not require MutinyArk on the compile-time classpath. Users who need the Mutiny/Vertx transport must switch to ark-quarkus-jackson-vertx (breaking change). The slim recorder throws a clear IllegalStateException when it encounters a Uni/Multi return type without the vertx add-on. Rest of the extension (JDK sync/async, Jackson serializer, @RegisterArkClient proxy generation, native image support) is unchanged. --- .../ark/quarkus/deployment/ArkProcessor.java | 13 ++--- .../deployment/ArkQuarkusExtensionTest.java | 15 ------ .../ark-quarkus-jackson/runtime/pom.xml | 17 ------- .../xyz/juandiii/ark/quarkus/ArkProducer.java | 22 --------- .../xyz/juandiii/ark/quarkus/ArkRecorder.java | 49 +++---------------- .../ark/quarkus/QuarkusVertxTlsResolver.java | 42 ---------------- .../ark/quarkus/VertxTlsResolver.java | 18 ------- 7 files changed, 11 insertions(+), 165 deletions(-) delete mode 100644 extensions/ark-quarkus-jackson/runtime/src/main/java/xyz/juandiii/ark/quarkus/QuarkusVertxTlsResolver.java delete mode 100644 extensions/ark-quarkus-jackson/runtime/src/main/java/xyz/juandiii/ark/quarkus/VertxTlsResolver.java diff --git a/extensions/ark-quarkus-jackson/deployment/src/main/java/xyz/juandiii/ark/quarkus/deployment/ArkProcessor.java b/extensions/ark-quarkus-jackson/deployment/src/main/java/xyz/juandiii/ark/quarkus/deployment/ArkProcessor.java index b4d2b09..ff12360 100644 --- a/extensions/ark-quarkus-jackson/deployment/src/main/java/xyz/juandiii/ark/quarkus/deployment/ArkProcessor.java +++ b/extensions/ark-quarkus-jackson/deployment/src/main/java/xyz/juandiii/ark/quarkus/deployment/ArkProcessor.java @@ -16,7 +16,6 @@ import xyz.juandiii.ark.quarkus.ArkProducer; import xyz.juandiii.ark.quarkus.ArkRecorder; import xyz.juandiii.ark.quarkus.QuarkusTlsResolver; -import xyz.juandiii.ark.quarkus.QuarkusVertxTlsResolver; import java.util.List; @@ -40,8 +39,7 @@ AdditionalBeanBuildItem registerBeans() { return AdditionalBeanBuildItem.builder() .addBeanClasses( ArkProducer.class, - QuarkusTlsResolver.class, - QuarkusVertxTlsResolver.class + QuarkusTlsResolver.class ) .setUnremovable() .build(); @@ -50,9 +48,7 @@ AdditionalBeanBuildItem registerBeans() { @BuildStep NativeImageResourceBuildItem nativeImageResources() { return new NativeImageResourceBuildItem( - "ark-version.properties", - "META-INF/vertx/vertx-version.txt", - "vertx-version.txt" + "ark-version.properties" ); } @@ -85,10 +81,7 @@ ReflectiveClassBuildItem proxyProviderClasses() { "xyz.juandiii.ark.proxy.SyncExecutionModelProvider", "xyz.juandiii.ark.proxy.jaxrs.JaxRsProxyProvider", "xyz.juandiii.ark.proxy.jaxrs.JaxRsAnnotationResolver", - "xyz.juandiii.ark.jaxrs.JaxRsParameterBinder", - "xyz.juandiii.ark.mutiny.proxy.MutinyExecutionModelProvider", - "xyz.juandiii.ark.mutiny.proxy.MutinyDispatchers", - "xyz.juandiii.ark.mutiny.proxy.MutinyReturnTypeHandler" + "xyz.juandiii.ark.jaxrs.JaxRsParameterBinder" ).constructors(true) .methods(true) .build(); diff --git a/extensions/ark-quarkus-jackson/deployment/src/test/java/xyz/juandiii/ark/quarkus/deployment/ArkQuarkusExtensionTest.java b/extensions/ark-quarkus-jackson/deployment/src/test/java/xyz/juandiii/ark/quarkus/deployment/ArkQuarkusExtensionTest.java index b4bd3bd..3e291f1 100644 --- a/extensions/ark-quarkus-jackson/deployment/src/test/java/xyz/juandiii/ark/quarkus/deployment/ArkQuarkusExtensionTest.java +++ b/extensions/ark-quarkus-jackson/deployment/src/test/java/xyz/juandiii/ark/quarkus/deployment/ArkQuarkusExtensionTest.java @@ -8,7 +8,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; import xyz.juandiii.ark.core.ArkClient; import xyz.juandiii.ark.core.JsonSerializer; -import xyz.juandiii.ark.mutiny.MutinyArkClient; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotSame; @@ -28,9 +27,6 @@ class ArkQuarkusExtensionTest { @Inject ArkClient.Builder anotherArkClientBuilder; - @Inject - MutinyArkClient.Builder mutinyArkClientBuilder; - @Test void givenExtension_whenStarted_thenJsonSerializerIsProduced() { assertNotNull(jsonSerializer); @@ -46,20 +42,9 @@ void givenTwoInjections_whenCompared_thenBuildersAreDifferentInstances() { assertNotSame(arkClientBuilder, anotherArkClientBuilder); } - @Test - void givenExtension_whenStarted_thenMutinyBuilderIsProduced() { - assertNotNull(mutinyArkClientBuilder); - } - @Test void givenSyncBuilder_whenBuild_thenCreatesClient() { var client = arkClientBuilder.baseUrl("http://localhost:8080").build(); assertNotNull(client); } - - @Test - void givenMutinyBuilder_whenBuild_thenCreatesClient() { - var client = mutinyArkClientBuilder.baseUrl("http://localhost:8080").build(); - assertNotNull(client); - } } diff --git a/extensions/ark-quarkus-jackson/runtime/pom.xml b/extensions/ark-quarkus-jackson/runtime/pom.xml index 39e92a4..05ba538 100644 --- a/extensions/ark-quarkus-jackson/runtime/pom.xml +++ b/extensions/ark-quarkus-jackson/runtime/pom.xml @@ -38,11 +38,6 @@ quarkus-arc - - io.quarkus - quarkus-vertx - - io.quarkus quarkus-jackson @@ -65,12 +60,6 @@ ${project.version} - - xyz.juandiii - ark-mutiny - ${project.version} - - xyz.juandiii ark-proxy-jaxrs @@ -88,12 +77,6 @@ ark-transport-jdk ${project.version} - - - xyz.juandiii - ark-transport-vertx-mutiny - ${project.version} - diff --git a/extensions/ark-quarkus-jackson/runtime/src/main/java/xyz/juandiii/ark/quarkus/ArkProducer.java b/extensions/ark-quarkus-jackson/runtime/src/main/java/xyz/juandiii/ark/quarkus/ArkProducer.java index 5a063b2..f2751ce 100644 --- a/extensions/ark-quarkus-jackson/runtime/src/main/java/xyz/juandiii/ark/quarkus/ArkProducer.java +++ b/extensions/ark-quarkus-jackson/runtime/src/main/java/xyz/juandiii/ark/quarkus/ArkProducer.java @@ -1,8 +1,6 @@ package xyz.juandiii.ark.quarkus; import io.quarkus.arc.DefaultBean; -import io.vertx.mutiny.core.Vertx; -import io.vertx.mutiny.ext.web.client.WebClient; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.Dependent; import jakarta.enterprise.inject.Produces; @@ -12,10 +10,7 @@ import xyz.juandiii.ark.jackson.classic.JacksonClassicSerializer; import xyz.juandiii.ark.core.JsonSerializer; import xyz.juandiii.ark.core.http.HttpTransport; -import xyz.juandiii.ark.mutiny.MutinyArkClient; -import xyz.juandiii.ark.mutiny.http.MutinyHttpTransport; import xyz.juandiii.ark.transport.jdk.ArkJdkSyncTransport; -import xyz.juandiii.ark.transport.vertx.mutiny.ArkVertxMutinyTransport; import java.net.http.HttpClient; @@ -41,13 +36,6 @@ public HttpTransport httpTransport() { return new ArkJdkSyncTransport(HttpClient.newBuilder().build()); } - @Produces - @Singleton - @DefaultBean - public MutinyHttpTransport mutinyHttpTransport(Vertx vertx) { - return new ArkVertxMutinyTransport(WebClient.create(vertx)); - } - @Produces @Dependent @DefaultBean @@ -56,14 +44,4 @@ public ArkClient.Builder arkClientBuilder(JsonSerializer serializer, HttpTranspo .serializer(serializer) .transport(transport); } - - @Produces - @Dependent - @DefaultBean - public MutinyArkClient.Builder mutinyArkClientBuilder(JsonSerializer serializer, - MutinyHttpTransport transport) { - return MutinyArkClient.builder() - .serializer(serializer) - .transport(transport); - } } diff --git a/extensions/ark-quarkus-jackson/runtime/src/main/java/xyz/juandiii/ark/quarkus/ArkRecorder.java b/extensions/ark-quarkus-jackson/runtime/src/main/java/xyz/juandiii/ark/quarkus/ArkRecorder.java index 812ecf2..33b759f 100644 --- a/extensions/ark-quarkus-jackson/runtime/src/main/java/xyz/juandiii/ark/quarkus/ArkRecorder.java +++ b/extensions/ark-quarkus-jackson/runtime/src/main/java/xyz/juandiii/ark/quarkus/ArkRecorder.java @@ -3,9 +3,6 @@ import io.quarkus.arc.Arc; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; -import io.vertx.ext.web.client.WebClientOptions; -import io.vertx.mutiny.core.Vertx; -import io.vertx.mutiny.ext.web.client.WebClient; import xyz.juandiii.ark.async.http.decorator.AsyncRetryOps; import xyz.juandiii.ark.core.ArkClient; import xyz.juandiii.ark.core.JsonSerializer; @@ -16,19 +13,17 @@ import xyz.juandiii.ark.core.interceptor.LoggingInterceptor; import xyz.juandiii.ark.core.interceptor.RequestInterceptor; import xyz.juandiii.ark.core.proxy.InterceptorResolver; -import xyz.juandiii.ark.mutiny.MutinyArkClient; import xyz.juandiii.ark.core.proxy.HttpVersion; import xyz.juandiii.ark.core.http.RetryPolicy; import xyz.juandiii.ark.core.ssl.InsecureSslContext; +import xyz.juandiii.ark.core.proxy.ArkProxy; import xyz.juandiii.ark.core.proxy.PropertyResolver; import xyz.juandiii.ark.core.proxy.RegisterArkClient; import xyz.juandiii.ark.core.proxy.TlsResolver; -import xyz.juandiii.ark.proxy.jaxrs.ArkJaxRsProxy; import xyz.juandiii.ark.quarkus.config.ArkClientNamedConfig; import xyz.juandiii.ark.quarkus.config.ArkClientsConfig; import xyz.juandiii.ark.transport.jdk.ArkJdkAsyncTransport; import xyz.juandiii.ark.transport.jdk.ArkJdkSyncTransport; -import xyz.juandiii.ark.transport.vertx.mutiny.ArkVertxMutinyTransport; import xyz.juandiii.ark.core.util.StringUtils; import javax.net.ssl.SSLContext; @@ -102,15 +97,10 @@ private static ResolvedConfig resolveConfig(String clientName, ArkClientNamedCon private static Object buildProxy(Class iface, JsonSerializer serializer, ResolvedConfig rc) { if (usesReactiveReturnTypes(iface)) { - MutinyArkClient.Builder builder = MutinyArkClient.builder() - .serializer(serializer) - .transport(buildMutinyTransport(rc)) - .baseUrl(rc.baseUrl()) - .httpVersion(rc.httpVersion()) - .connectTimeout(rc.connectTimeout()) - .readTimeout(rc.readTimeout()); - applyInterceptors(builder, rc); - return ArkJaxRsProxy.create(iface, builder.build()); + throw new IllegalStateException( + "Interface " + iface.getName() + " has methods returning Uni/Multi but " + + "ark-quarkus-jackson-vertx is not on the classpath. Add the vertx add-on: " + + "xyz.juandiii:ark-quarkus-jackson-vertx"); } else if (usesAsyncReturnTypes(iface)) { SSLContext sslContext = resolveSslContext(rc.clientName(), rc.tlsConfigName(), rc.trustAll()); var jdk = new ArkJdkAsyncTransport(buildHttpClient(rc.httpVersion(), rc.connectTimeout(), sslContext)); @@ -125,7 +115,7 @@ private static Object buildProxy(Class iface, JsonSerializer serializer, Reso .readTimeout(rc.readTimeout()) .requestInterceptor(defaultTimeout(rc.readTimeout())); applyInterceptors(builder, rc); - return ArkJaxRsProxy.create(iface, builder.build()); + return ArkProxy.create(iface, builder.build()); } SSLContext sslContext = resolveSslContext(rc.clientName(), rc.tlsConfigName(), rc.trustAll()); var jdkTransport = new ArkJdkSyncTransport(buildHttpClient(rc.httpVersion(), rc.connectTimeout(), sslContext)); @@ -140,7 +130,7 @@ private static Object buildProxy(Class iface, JsonSerializer serializer, Reso .readTimeout(rc.readTimeout()) .requestInterceptor(defaultTimeout(rc.readTimeout())); applyInterceptors(builder, rc); - return ArkJaxRsProxy.create(iface, builder.build()); + return ArkProxy.create(iface, builder.build()); } private static > void applyInterceptors( @@ -196,29 +186,6 @@ private static HttpClient buildHttpClient(HttpVersion httpVersion, int connectTi return httpBuilder.build(); } - private static ArkVertxMutinyTransport buildMutinyTransport(ResolvedConfig rc) { - Vertx vertx = Arc.container().instance(Vertx.class).get(); - WebClientOptions options = new WebClientOptions() - .setProtocolVersion(rc.httpVersion() == HttpVersion.HTTP_2 - ? io.vertx.core.http.HttpVersion.HTTP_2 - : io.vertx.core.http.HttpVersion.HTTP_1_1) - .setConnectTimeout(rc.connectTimeout() * 1000) - .setIdleTimeout(rc.readTimeout()); - - if (rc.trustAll()) { - InsecureSslContext.warnTrustAll(rc.clientName()); - options.setSsl(true).setTrustAll(true).setVerifyHost(false); - } else if (StringUtils.isNotEmpty(rc.tlsConfigName())) { - VertxTlsResolver vertxTlsResolver = - Arc.container().instance(VertxTlsResolver.class).get(); - options.setSsl(true); - vertxTlsResolver.resolveTrustOptions(rc.tlsConfigName()).ifPresent(options::setTrustOptions); - vertxTlsResolver.resolveKeyCertOptions(rc.tlsConfigName()).ifPresent(options::setKeyCertOptions); - } - - return new ArkVertxMutinyTransport(WebClient.create(vertx, options)); - } - private static RequestInterceptor defaultTimeout(int readTimeout) { return ctx -> { if (ctx.timeout() == null) { @@ -246,4 +213,4 @@ private static boolean usesAsyncReturnTypes(Class iface) { } return false; } -} \ No newline at end of file +} diff --git a/extensions/ark-quarkus-jackson/runtime/src/main/java/xyz/juandiii/ark/quarkus/QuarkusVertxTlsResolver.java b/extensions/ark-quarkus-jackson/runtime/src/main/java/xyz/juandiii/ark/quarkus/QuarkusVertxTlsResolver.java deleted file mode 100644 index 1e461e3..0000000 --- a/extensions/ark-quarkus-jackson/runtime/src/main/java/xyz/juandiii/ark/quarkus/QuarkusVertxTlsResolver.java +++ /dev/null @@ -1,42 +0,0 @@ -package xyz.juandiii.ark.quarkus; - -import io.quarkus.tls.TlsConfiguration; -import io.quarkus.tls.TlsConfigurationRegistry; -import io.vertx.core.net.KeyCertOptions; -import io.vertx.core.net.TrustOptions; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import xyz.juandiii.ark.core.exceptions.ArkException; - -import java.util.Optional; - -/** - * Resolves TLS configuration for Vert.x transports using Quarkus TLS Registry. - * Returns native Vert.x TrustOptions/KeyCertOptions instead of SSLContext. - * - * @author Juan Diego Lopez V. - */ -@ApplicationScoped -public class QuarkusVertxTlsResolver implements VertxTlsResolver { - - @Inject - TlsConfigurationRegistry tlsRegistry; - - @Override - public Optional resolveTrustOptions(String tlsConfigurationName) { - return Optional.ofNullable(getTlsConfig(tlsConfigurationName).getTrustStoreOptions()); - } - - @Override - public Optional resolveKeyCertOptions(String tlsConfigurationName) { - return Optional.ofNullable(getTlsConfig(tlsConfigurationName).getKeyStoreOptions()); - } - - private TlsConfiguration getTlsConfig(String tlsConfigurationName) { - return tlsRegistry.get(tlsConfigurationName) - .orElseThrow(() -> new ArkException( - "TLS configuration not found: " + tlsConfigurationName - + ". Define it in application.properties: quarkus.tls.\"" - + tlsConfigurationName + "\".trust-store.pem.certs=...")); - } -} \ No newline at end of file diff --git a/extensions/ark-quarkus-jackson/runtime/src/main/java/xyz/juandiii/ark/quarkus/VertxTlsResolver.java b/extensions/ark-quarkus-jackson/runtime/src/main/java/xyz/juandiii/ark/quarkus/VertxTlsResolver.java deleted file mode 100644 index 534717c..0000000 --- a/extensions/ark-quarkus-jackson/runtime/src/main/java/xyz/juandiii/ark/quarkus/VertxTlsResolver.java +++ /dev/null @@ -1,18 +0,0 @@ -package xyz.juandiii.ark.quarkus; - -import io.vertx.core.net.KeyCertOptions; -import io.vertx.core.net.TrustOptions; - -import java.util.Optional; - -/** - * Interface for resolving TLS configuration as Vert.x native options. - * - * @author Juan Diego Lopez V. - */ -public interface VertxTlsResolver { - - Optional resolveTrustOptions(String tlsConfigurationName); - - Optional resolveKeyCertOptions(String tlsConfigurationName); -} \ No newline at end of file From 7b139fb7a0baa4cacbe60dd85b250ef3fa30c9b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Diego=20L=C3=B3pez?= Date: Tue, 30 Jun 2026 21:54:35 -0400 Subject: [PATCH 08/11] feat(quarkus-jackson-vertx): add ark-quarkus-jackson-vertx add-on for Mutiny transport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces a new Quarkus extension that layers the Vert.x Mutiny transport on top of the slim ark-quarkus-jackson (JDK-only) extension. Composition: - Runtime module (ark-quarkus-jackson-vertx) depends on ark-quarkus-jackson + quarkus-vertx + ark-mutiny + ark-transport-vertx-mutiny - Adds ArkVertxProducer (MutinyHttpTransport + MutinyArkClient.Builder CDI producers), ArkVertxRecorder (Mutiny client factory), and VertxTlsResolver / QuarkusVertxTlsResolver (moved from the slim extension, now under xyz.juandiii.ark.quarkus.vertx.*) - Deployment module (ark-quarkus-jackson-vertx-deployment) registers the Vertx-specific beans, native image proxy hints, and vertx-version.txt resources — mirrors the slim ArkProcessor pattern with vertx-only content Interface partitioning strategy: to avoid duplicate synthetic bean registrations, the slim ArkProcessor skips @RegisterArkClient interfaces with Uni/Multi return types (detected via Jandex), and the vertx processor registers only those. Non-reactive interfaces stay with the slim extension. Users who need the Mutiny transport switch from: ark-quarkus-jackson to: ark-quarkus-jackson-vertx The slim extension continues to work as before for JDK-only usage. --- .../deployment/pom.xml | 99 +++++++++++ .../vertx/deployment/ArkVertxProcessor.java | 114 +++++++++++++ extensions/ark-quarkus-jackson-vertx/pom.xml | 20 +++ .../ark-quarkus-jackson-vertx/runtime/pom.xml | 94 +++++++++++ .../ark/quarkus/vertx/ArkVertxProducer.java | 39 +++++ .../ark/quarkus/vertx/ArkVertxRecorder.java | 157 ++++++++++++++++++ .../vertx/QuarkusVertxTlsResolver.java | 42 +++++ .../ark/quarkus/vertx/VertxTlsResolver.java | 18 ++ .../resources/META-INF/quarkus-extension.yaml | 15 ++ .../ark/quarkus/deployment/ArkProcessor.java | 17 ++ pom.xml | 1 + 11 files changed, 616 insertions(+) create mode 100644 extensions/ark-quarkus-jackson-vertx/deployment/pom.xml create mode 100644 extensions/ark-quarkus-jackson-vertx/deployment/src/main/java/xyz/juandiii/ark/quarkus/vertx/deployment/ArkVertxProcessor.java create mode 100644 extensions/ark-quarkus-jackson-vertx/pom.xml create mode 100644 extensions/ark-quarkus-jackson-vertx/runtime/pom.xml create mode 100644 extensions/ark-quarkus-jackson-vertx/runtime/src/main/java/xyz/juandiii/ark/quarkus/vertx/ArkVertxProducer.java create mode 100644 extensions/ark-quarkus-jackson-vertx/runtime/src/main/java/xyz/juandiii/ark/quarkus/vertx/ArkVertxRecorder.java create mode 100644 extensions/ark-quarkus-jackson-vertx/runtime/src/main/java/xyz/juandiii/ark/quarkus/vertx/QuarkusVertxTlsResolver.java create mode 100644 extensions/ark-quarkus-jackson-vertx/runtime/src/main/java/xyz/juandiii/ark/quarkus/vertx/VertxTlsResolver.java create mode 100644 extensions/ark-quarkus-jackson-vertx/runtime/src/main/resources/META-INF/quarkus-extension.yaml diff --git a/extensions/ark-quarkus-jackson-vertx/deployment/pom.xml b/extensions/ark-quarkus-jackson-vertx/deployment/pom.xml new file mode 100644 index 0000000..9fd7ce1 --- /dev/null +++ b/extensions/ark-quarkus-jackson-vertx/deployment/pom.xml @@ -0,0 +1,99 @@ + + + 4.0.0 + + xyz.juandiii + ark-quarkus-jackson-vertx-parent + 1.0.8-SNAPSHOT + + ark-quarkus-jackson-vertx-deployment + ark-quarkus-jackson-vertx - Deployment + Quarkus extension deployment for the Ark Vert.x Mutiny transport add-on + https://github.com/juandiii/ark + + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + Juan Diego + https://github.com/juandiii + + + + + scm:git:git://github.com/juandiii/ark.git + scm:git:ssh://github.com:juandiii/ark.git + https://github.com/juandiii/ark + + + + + xyz.juandiii + ark-quarkus-jackson-deployment + ${project.version} + + + + xyz.juandiii + ark-quarkus-jackson-vertx + ${project.version} + + + + io.quarkus + quarkus-arc-deployment + + + + io.quarkus + quarkus-vertx-deployment + + + + io.quarkus + quarkus-junit-internal + test + + + + + + + org.jacoco + jacoco-maven-plugin + + true + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${quarkus.version} + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + org.jboss.logmanager.LogManager + + + + + + diff --git a/extensions/ark-quarkus-jackson-vertx/deployment/src/main/java/xyz/juandiii/ark/quarkus/vertx/deployment/ArkVertxProcessor.java b/extensions/ark-quarkus-jackson-vertx/deployment/src/main/java/xyz/juandiii/ark/quarkus/vertx/deployment/ArkVertxProcessor.java new file mode 100644 index 0000000..a030501 --- /dev/null +++ b/extensions/ark-quarkus-jackson-vertx/deployment/src/main/java/xyz/juandiii/ark/quarkus/vertx/deployment/ArkVertxProcessor.java @@ -0,0 +1,114 @@ +package xyz.juandiii.ark.quarkus.vertx.deployment; + +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import jakarta.enterprise.context.ApplicationScoped; +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationValue; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.MethodInfo; +import xyz.juandiii.ark.quarkus.vertx.ArkVertxProducer; +import xyz.juandiii.ark.quarkus.vertx.ArkVertxRecorder; +import xyz.juandiii.ark.quarkus.vertx.QuarkusVertxTlsResolver; + +/** + * Quarkus deployment processor for the Ark Vert.x Mutiny transport add-on. + * Registers Mutiny CDI producers, Vertx TLS resolver, native image hints for the + * Mutiny proxy provider classes, and synthetic beans for @RegisterArkClient + * interfaces whose methods return Uni/Multi. + * + * @author Juan Diego Lopez V. + */ +public class ArkVertxProcessor { + + private static final String FEATURE = "ark-jackson-vertx"; + private static final DotName ARK_CLIENT = DotName.createSimple("xyz.juandiii.ark.core.proxy.RegisterArkClient"); + private static final DotName MUTINY_UNI = DotName.createSimple("io.smallrye.mutiny.Uni"); + private static final DotName MUTINY_MULTI = DotName.createSimple("io.smallrye.mutiny.Multi"); + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(FEATURE); + } + + @BuildStep + AdditionalBeanBuildItem registerVertxBeans() { + return AdditionalBeanBuildItem.builder() + .addBeanClasses( + ArkVertxProducer.class, + QuarkusVertxTlsResolver.class + ) + .setUnremovable() + .build(); + } + + @BuildStep + NativeImageResourceBuildItem vertxNativeResources() { + return new NativeImageResourceBuildItem( + "META-INF/vertx/vertx-version.txt", + "vertx-version.txt" + ); + } + + @BuildStep + ReflectiveClassBuildItem mutinyProxyProviderClasses() { + return ReflectiveClassBuildItem.builder( + "xyz.juandiii.ark.mutiny.proxy.MutinyExecutionModelProvider", + "xyz.juandiii.ark.mutiny.proxy.MutinyDispatchers", + "xyz.juandiii.ark.mutiny.proxy.MutinyReturnTypeHandler" + ).constructors(true) + .methods(true) + .build(); + } + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + void createReactiveArkClientBeans(ArkVertxRecorder recorder, + CombinedIndexBuildItem combinedIndex, + BuildProducer syntheticBeans) { + IndexView index = combinedIndex.getIndex(); + + for (AnnotationInstance instance : index.getAnnotations(ARK_CLIENT)) { + ClassInfo classInfo = instance.target().asClass(); + // Only register interfaces with Uni/Multi return types — non-reactive + // interfaces are registered by the slim ark-quarkus-jackson extension. + if (!hasReactiveReturnType(classInfo)) continue; + String className = classInfo.name().toString(); + String configKey = stringValue(instance, "configKey", ""); + + syntheticBeans.produce( + SyntheticBeanBuildItem.configure(DotName.createSimple(className)) + .scope(ApplicationScoped.class) + .unremovable() + .setRuntimeInit() + .supplier(recorder.createMutinyArkClient(className, configKey)) + .done() + ); + } + } + + private static boolean hasReactiveReturnType(ClassInfo classInfo) { + for (MethodInfo method : classInfo.methods()) { + DotName returnName = method.returnType().name(); + if (MUTINY_UNI.equals(returnName) || MUTINY_MULTI.equals(returnName)) { + return true; + } + } + return false; + } + + private static String stringValue(AnnotationInstance instance, String name, String defaultValue) { + AnnotationValue value = instance.value(name); + return value != null ? value.asString() : defaultValue; + } +} diff --git a/extensions/ark-quarkus-jackson-vertx/pom.xml b/extensions/ark-quarkus-jackson-vertx/pom.xml new file mode 100644 index 0000000..bfc2e86 --- /dev/null +++ b/extensions/ark-quarkus-jackson-vertx/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + + xyz.juandiii + ark + 1.0.8-SNAPSHOT + ../../pom.xml + + ark-quarkus-jackson-vertx-parent + ark-quarkus-jackson-vertx + Quarkus extension parent for Ark Vert.x Mutiny transport add-on (adds Uni/Multi support to ark-quarkus-jackson) + pom + + + runtime + deployment + + diff --git a/extensions/ark-quarkus-jackson-vertx/runtime/pom.xml b/extensions/ark-quarkus-jackson-vertx/runtime/pom.xml new file mode 100644 index 0000000..b7714f0 --- /dev/null +++ b/extensions/ark-quarkus-jackson-vertx/runtime/pom.xml @@ -0,0 +1,94 @@ + + + 4.0.0 + + xyz.juandiii + ark-quarkus-jackson-vertx-parent + 1.0.8-SNAPSHOT + + ark-quarkus-jackson-vertx + ark-quarkus-jackson-vertx - Runtime + Quarkus extension runtime for Ark Vert.x Mutiny transport (add-on on top of ark-quarkus-jackson) + https://github.com/juandiii/ark + + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + Juan Diego + https://github.com/juandiii + + + + + scm:git:git://github.com/juandiii/ark.git + scm:git:ssh://github.com:juandiii/ark.git + https://github.com/juandiii/ark + + + + + xyz.juandiii + ark-quarkus-jackson + ${project.version} + + + + io.quarkus + quarkus-vertx + + + + xyz.juandiii + ark-mutiny + ${project.version} + + + + xyz.juandiii + ark-transport-vertx-mutiny + ${project.version} + + + + + + + io.quarkus + quarkus-extension-maven-plugin + ${quarkus.version} + true + + + compile + + extension-descriptor + + + ${project.groupId}:ark-quarkus-jackson-vertx-deployment:${project.version} + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${quarkus.version} + + + + + + + diff --git a/extensions/ark-quarkus-jackson-vertx/runtime/src/main/java/xyz/juandiii/ark/quarkus/vertx/ArkVertxProducer.java b/extensions/ark-quarkus-jackson-vertx/runtime/src/main/java/xyz/juandiii/ark/quarkus/vertx/ArkVertxProducer.java new file mode 100644 index 0000000..2e0b8a4 --- /dev/null +++ b/extensions/ark-quarkus-jackson-vertx/runtime/src/main/java/xyz/juandiii/ark/quarkus/vertx/ArkVertxProducer.java @@ -0,0 +1,39 @@ +package xyz.juandiii.ark.quarkus.vertx; + +import io.quarkus.arc.DefaultBean; +import io.vertx.mutiny.core.Vertx; +import io.vertx.mutiny.ext.web.client.WebClient; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Singleton; +import xyz.juandiii.ark.core.JsonSerializer; +import xyz.juandiii.ark.mutiny.MutinyArkClient; +import xyz.juandiii.ark.mutiny.http.MutinyHttpTransport; +import xyz.juandiii.ark.transport.vertx.mutiny.ArkVertxMutinyTransport; + +/** + * CDI producer for the Ark Vert.x Mutiny transport add-on beans in Quarkus. + * + * @author Juan Diego Lopez V. + */ +@ApplicationScoped +public class ArkVertxProducer { + + @Produces + @Singleton + @DefaultBean + public MutinyHttpTransport mutinyHttpTransport(Vertx vertx) { + return new ArkVertxMutinyTransport(WebClient.create(vertx)); + } + + @Produces + @Dependent + @DefaultBean + public MutinyArkClient.Builder mutinyArkClientBuilder(JsonSerializer serializer, + MutinyHttpTransport transport) { + return MutinyArkClient.builder() + .serializer(serializer) + .transport(transport); + } +} diff --git a/extensions/ark-quarkus-jackson-vertx/runtime/src/main/java/xyz/juandiii/ark/quarkus/vertx/ArkVertxRecorder.java b/extensions/ark-quarkus-jackson-vertx/runtime/src/main/java/xyz/juandiii/ark/quarkus/vertx/ArkVertxRecorder.java new file mode 100644 index 0000000..06550de --- /dev/null +++ b/extensions/ark-quarkus-jackson-vertx/runtime/src/main/java/xyz/juandiii/ark/quarkus/vertx/ArkVertxRecorder.java @@ -0,0 +1,157 @@ +package xyz.juandiii.ark.quarkus.vertx; + +import io.quarkus.arc.Arc; +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; +import io.vertx.ext.web.client.WebClientOptions; +import io.vertx.mutiny.core.Vertx; +import io.vertx.mutiny.ext.web.client.WebClient; +import xyz.juandiii.ark.core.AbstractArkBuilder; +import xyz.juandiii.ark.core.JsonSerializer; +import xyz.juandiii.ark.core.http.RetryPolicy; +import xyz.juandiii.ark.core.interceptor.LoggingInterceptor; +import xyz.juandiii.ark.core.proxy.ArkProxy; +import xyz.juandiii.ark.core.proxy.HttpVersion; +import xyz.juandiii.ark.core.proxy.InterceptorResolver; +import xyz.juandiii.ark.core.proxy.PropertyResolver; +import xyz.juandiii.ark.core.proxy.RegisterArkClient; +import xyz.juandiii.ark.core.ssl.InsecureSslContext; +import xyz.juandiii.ark.core.util.StringUtils; +import xyz.juandiii.ark.mutiny.MutinyArkClient; +import xyz.juandiii.ark.quarkus.config.ArkClientNamedConfig; +import xyz.juandiii.ark.quarkus.config.ArkClientsConfig; +import xyz.juandiii.ark.transport.vertx.mutiny.ArkVertxMutinyTransport; + +import java.time.Duration; +import java.util.Map; +import java.util.function.Supplier; + +/** + * Quarkus recorder that creates Mutiny-based Ark proxy client beans at runtime for + * @RegisterArkClient interfaces whose methods return Uni/Multi. + * + * @author Juan Diego Lopez V. + */ +@Recorder +public class ArkVertxRecorder { + + private final RuntimeValue clientsConfigValue; + + public ArkVertxRecorder(RuntimeValue clientsConfigValue) { + this.clientsConfigValue = clientsConfigValue; + } + + public Supplier createMutinyArkClient(String interfaceName, String configKey) { + return () -> { + try { + ArkClientsConfig clientsConfig = clientsConfigValue.getValue(); + Class iface = Thread.currentThread().getContextClassLoader().loadClass(interfaceName); + JsonSerializer serializer = Arc.container().instance(JsonSerializer.class).get(); + + String key = StringUtils.isNotEmpty(configKey) ? configKey : interfaceName; + ArkClientNamedConfig config = clientsConfig.client().get(key); + RegisterArkClient annotation = iface.getAnnotation(RegisterArkClient.class); + + ResolvedConfig resolved = resolveConfig(key, config, annotation, clientsConfig.loggingLevel()); + return buildProxy(iface, serializer, resolved); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Failed to create Mutiny Ark client for " + interfaceName, e); + } + }; + } + + private record ResolvedConfig(String clientName, String baseUrl, HttpVersion httpVersion, + int connectTimeout, int readTimeout, String tlsConfigName, + boolean trustAll, boolean throwOnError, + Map headers, + Class[] interceptorClasses, + RetryPolicy retryPolicy, + LoggingInterceptor.Level loggingLevel) {} + + private static ResolvedConfig resolveConfig(String clientName, ArkClientNamedConfig config, + RegisterArkClient annotation, + LoggingInterceptor.Level loggingLevel) { + return new ResolvedConfig( + clientName, + resolveBaseUrl(config, annotation), + config != null ? config.httpVersion() : annotation.httpVersion(), + config != null ? config.connectTimeout() : annotation.connectTimeout(), + config != null ? config.readTimeout() : annotation.readTimeout(), + config != null ? config.tlsConfigurationName().orElse(null) : null, + config != null && config.trustAll(), + config == null || config.throwOnError(), + config != null ? config.headers() : Map.of(), + annotation != null ? annotation.interceptors() : new Class[0], + resolveRetryPolicy(config), + loggingLevel + ); + } + + private static Object buildProxy(Class iface, JsonSerializer serializer, ResolvedConfig rc) { + MutinyArkClient.Builder builder = MutinyArkClient.builder() + .serializer(serializer) + .transport(buildMutinyTransport(rc)) + .baseUrl(rc.baseUrl()) + .httpVersion(rc.httpVersion()) + .connectTimeout(rc.connectTimeout()) + .readTimeout(rc.readTimeout()); + applyInterceptors(builder, rc); + return ArkProxy.create(iface, builder.build()); + } + + private static > void applyInterceptors( + B builder, ResolvedConfig rc) { + InterceptorResolver.applyHeaders(builder, rc.headers()); + InterceptorResolver.applyInterceptors(builder, rc.interceptorClasses(), + clazz -> Arc.container().instance(clazz).get()); + LoggingInterceptor.apply(builder, rc.loggingLevel()); + builder.throwOnError(rc.throwOnError()); + } + + private static String resolveBaseUrl(ArkClientNamedConfig config, RegisterArkClient annotation) { + if (config != null && config.baseUrl().isPresent()) { + return config.baseUrl().get(); + } + if (annotation == null) return ""; + return PropertyResolver.resolve(annotation.baseUrl(), + key -> org.eclipse.microprofile.config.ConfigProvider.getConfig() + .getOptionalValue(key, String.class).orElse(null)); + } + + private static RetryPolicy resolveRetryPolicy(ArkClientNamedConfig config) { + if (config == null || config.retry().maxAttempts() <= 1) return null; + ArkClientNamedConfig.RetryConfig r = config.retry(); + return RetryPolicy.builder() + .maxAttempts(r.maxAttempts()) + .delay(Duration.ofMillis(r.delay())) + .multiplier(r.multiplier()) + .maxDelay(Duration.ofMillis(r.maxDelay())) + .retryOn(r.retryOn()) + .retryOnException(r.retryOnException()) + .retryPost(r.retryPost()) + .build(); + } + + private static ArkVertxMutinyTransport buildMutinyTransport(ResolvedConfig rc) { + Vertx vertx = Arc.container().instance(Vertx.class).get(); + WebClientOptions options = new WebClientOptions() + .setProtocolVersion(rc.httpVersion() == HttpVersion.HTTP_2 + ? io.vertx.core.http.HttpVersion.HTTP_2 + : io.vertx.core.http.HttpVersion.HTTP_1_1) + .setConnectTimeout(rc.connectTimeout() * 1000) + .setIdleTimeout(rc.readTimeout()); + + if (rc.trustAll()) { + InsecureSslContext.warnTrustAll(rc.clientName()); + options.setSsl(true).setTrustAll(true).setVerifyHost(false); + } else if (StringUtils.isNotEmpty(rc.tlsConfigName())) { + VertxTlsResolver vertxTlsResolver = + Arc.container().instance(VertxTlsResolver.class).get(); + options.setSsl(true); + vertxTlsResolver.resolveTrustOptions(rc.tlsConfigName()).ifPresent(options::setTrustOptions); + vertxTlsResolver.resolveKeyCertOptions(rc.tlsConfigName()).ifPresent(options::setKeyCertOptions); + } + + return new ArkVertxMutinyTransport(WebClient.create(vertx, options)); + } +} diff --git a/extensions/ark-quarkus-jackson-vertx/runtime/src/main/java/xyz/juandiii/ark/quarkus/vertx/QuarkusVertxTlsResolver.java b/extensions/ark-quarkus-jackson-vertx/runtime/src/main/java/xyz/juandiii/ark/quarkus/vertx/QuarkusVertxTlsResolver.java new file mode 100644 index 0000000..c02280e --- /dev/null +++ b/extensions/ark-quarkus-jackson-vertx/runtime/src/main/java/xyz/juandiii/ark/quarkus/vertx/QuarkusVertxTlsResolver.java @@ -0,0 +1,42 @@ +package xyz.juandiii.ark.quarkus.vertx; + +import io.quarkus.tls.TlsConfiguration; +import io.quarkus.tls.TlsConfigurationRegistry; +import io.vertx.core.net.KeyCertOptions; +import io.vertx.core.net.TrustOptions; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import xyz.juandiii.ark.core.exceptions.ArkException; + +import java.util.Optional; + +/** + * Resolves TLS configuration for Vert.x transports using Quarkus TLS Registry. + * Returns native Vert.x TrustOptions/KeyCertOptions instead of SSLContext. + * + * @author Juan Diego Lopez V. + */ +@ApplicationScoped +public class QuarkusVertxTlsResolver implements VertxTlsResolver { + + @Inject + TlsConfigurationRegistry tlsRegistry; + + @Override + public Optional resolveTrustOptions(String tlsConfigurationName) { + return Optional.ofNullable(getTlsConfig(tlsConfigurationName).getTrustStoreOptions()); + } + + @Override + public Optional resolveKeyCertOptions(String tlsConfigurationName) { + return Optional.ofNullable(getTlsConfig(tlsConfigurationName).getKeyStoreOptions()); + } + + private TlsConfiguration getTlsConfig(String tlsConfigurationName) { + return tlsRegistry.get(tlsConfigurationName) + .orElseThrow(() -> new ArkException( + "TLS configuration not found: " + tlsConfigurationName + + ". Define it in application.properties: quarkus.tls.\"" + + tlsConfigurationName + "\".trust-store.pem.certs=...")); + } +} diff --git a/extensions/ark-quarkus-jackson-vertx/runtime/src/main/java/xyz/juandiii/ark/quarkus/vertx/VertxTlsResolver.java b/extensions/ark-quarkus-jackson-vertx/runtime/src/main/java/xyz/juandiii/ark/quarkus/vertx/VertxTlsResolver.java new file mode 100644 index 0000000..0e2826f --- /dev/null +++ b/extensions/ark-quarkus-jackson-vertx/runtime/src/main/java/xyz/juandiii/ark/quarkus/vertx/VertxTlsResolver.java @@ -0,0 +1,18 @@ +package xyz.juandiii.ark.quarkus.vertx; + +import io.vertx.core.net.KeyCertOptions; +import io.vertx.core.net.TrustOptions; + +import java.util.Optional; + +/** + * Interface for resolving TLS configuration as Vert.x native options. + * + * @author Juan Diego Lopez V. + */ +public interface VertxTlsResolver { + + Optional resolveTrustOptions(String tlsConfigurationName); + + Optional resolveKeyCertOptions(String tlsConfigurationName); +} diff --git a/extensions/ark-quarkus-jackson-vertx/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/ark-quarkus-jackson-vertx/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000..870f14a --- /dev/null +++ b/extensions/ark-quarkus-jackson-vertx/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,15 @@ +--- +name: "Ark HTTP - Vert.x Mutiny transport" +description: "Vert.x Mutiny transport add-on for the Ark HTTP client Quarkus extension" +metadata: + keywords: + - "ark" + - "http" + - "http-client" + - "vertx" + - "mutiny" + - "reactive" + categories: + - "web" + status: "stable" + guide: "https://github.com/juandiii/ark/blob/main/docs/quarkus-jackson.md" diff --git a/extensions/ark-quarkus-jackson/deployment/src/main/java/xyz/juandiii/ark/quarkus/deployment/ArkProcessor.java b/extensions/ark-quarkus-jackson/deployment/src/main/java/xyz/juandiii/ark/quarkus/deployment/ArkProcessor.java index ff12360..6143db8 100644 --- a/extensions/ark-quarkus-jackson/deployment/src/main/java/xyz/juandiii/ark/quarkus/deployment/ArkProcessor.java +++ b/extensions/ark-quarkus-jackson/deployment/src/main/java/xyz/juandiii/ark/quarkus/deployment/ArkProcessor.java @@ -28,6 +28,8 @@ public class ArkProcessor { private static final String FEATURE = "ark-jackson"; private static final DotName ARK_CLIENT = DotName.createSimple("xyz.juandiii.ark.core.proxy.RegisterArkClient"); + private static final DotName MUTINY_UNI = DotName.createSimple("io.smallrye.mutiny.Uni"); + private static final DotName MUTINY_MULTI = DotName.createSimple("io.smallrye.mutiny.Multi"); @BuildStep FeatureBuildItem feature() { @@ -96,6 +98,11 @@ void createArkClientBeans(ArkRecorder recorder, for (AnnotationInstance instance : index.getAnnotations(ARK_CLIENT)) { ClassInfo classInfo = instance.target().asClass(); + // Skip interfaces with Uni/Multi return types — they are registered by + // the ark-quarkus-jackson-vertx add-on's ArkVertxProcessor. If the add-on + // is not on the classpath, the interface simply won't have a bean and + // CDI will fail at injection time with a clear "no bean" message. + if (hasReactiveReturnType(classInfo)) continue; String className = classInfo.name().toString(); String configKey = stringValue(instance, "configKey", ""); @@ -110,6 +117,16 @@ void createArkClientBeans(ArkRecorder recorder, } } + private static boolean hasReactiveReturnType(ClassInfo classInfo) { + for (MethodInfo method : classInfo.methods()) { + DotName returnName = method.returnType().name(); + if (MUTINY_UNI.equals(returnName) || MUTINY_MULTI.equals(returnName)) { + return true; + } + } + return false; + } + private static String stringValue(AnnotationInstance instance, String name, String defaultValue) { AnnotationValue value = instance.value(name); return value != null ? value.asString() : defaultValue; diff --git a/pom.xml b/pom.xml index f5867ad..26a80c8 100644 --- a/pom.xml +++ b/pom.xml @@ -102,6 +102,7 @@ starters/ark-spring-boot-starter starters/ark-spring-boot-starter-webflux extensions/ark-quarkus-jackson + extensions/ark-quarkus-jackson-vertx From 4b15f9a38e8707881338e5052367332ca77b3398 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Diego=20L=C3=B3pez?= Date: Tue, 30 Jun 2026 21:57:10 -0400 Subject: [PATCH 09/11] docs(quarkus): document ark-quarkus-jackson slim vs -vertx split (breaking change) - docs/quarkus-jackson.md: adds 'Which extension?' section explaining the two artifacts and when to pick each, and splits the CDI bean table between slim-only and vertx-add-on beans - README.md: Quarkus collapsible now shows both artifact IDs - CHANGELOG.md: [Unreleased] Breaking section documents: - Split of ark-quarkus-jackson into slim + -vertx add-on - Package move for VertxTlsResolver/QuarkusVertxTlsResolver from xyz.juandiii.ark.quarkus.* to xyz.juandiii.ark.quarkus.vertx.* - Slim extension no longer transitively pulls Vert.x deps --- CHANGELOG.md | 12 ++++++++++++ README.md | 11 +++++++++++ docs/quarkus-jackson.md | 38 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f4b505..6ef062e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,18 @@ ## [Unreleased] +### Breaking + +- **Quarkus extension split**: `ark-quarkus-jackson` no longer bundles the Vert.x Mutiny + transport. Users who relied on `Uni`/`Multi` proxy methods, `MutinyHttpTransport`, + `MutinyArkClient.Builder`, or `VertxTlsResolver`/`QuarkusVertxTlsResolver` must switch to + the new `ark-quarkus-jackson-vertx` artifact. +- The classes `xyz.juandiii.ark.quarkus.VertxTlsResolver` and + `xyz.juandiii.ark.quarkus.QuarkusVertxTlsResolver` moved to package + `xyz.juandiii.ark.quarkus.vertx.*`. Update imports if you referenced them directly. +- Slim `ark-quarkus-jackson` gets JDK-only transport and drops ~5-10 MB of transitive deps + (quarkus-vertx, ark-mutiny, ark-transport-vertx-mutiny). + ### Changed - **Project structure**: Maven modules are now grouped under semantic diff --git a/README.md b/README.md index f8e215f..4655755 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,8 @@ Proxy methods return `Mono` / `Flux`; transport is Reactor Netty by defaul
Quarkus (JVM + native) +Slim (JDK transport only): + ```xml xyz.juandiii @@ -173,6 +175,15 @@ Proxy methods return `Mono` / `Flux`; transport is Reactor Netty by defaul ``` +With Vert.x Mutiny transport (adds `Uni` / `Multi` proxy method support): + +```xml + + xyz.juandiii + ark-quarkus-jackson-vertx + +``` + Annotate interfaces with `@RegisterArkClient` + JAX-RS (`@Path` / `@GET` / `@POST`) — or use Spring's `@HttpExchange` if you prefer. Build-time reflection + proxy hints emitted for native. diff --git a/docs/quarkus-jackson.md b/docs/quarkus-jackson.md index 8163302..7223285 100644 --- a/docs/quarkus-jackson.md +++ b/docs/quarkus-jackson.md @@ -4,6 +4,35 @@ The `ark-quarkus-jackson` extension auto-configures Ark HTTP clients for Quarkus --- +## Which extension? + +Ark ships two Quarkus extensions: + +| Extension | Includes | When to pick | +|---|---|---| +| `ark-quarkus-jackson` | JDK transport (sync + async), Jackson, TLS registry, proxy scanning | Default. Lean, no Vert.x, smaller native images. | +| `ark-quarkus-jackson-vertx` | Everything above PLUS Vert.x Mutiny transport, Vert.x TLS resolver, Mutiny client builder | Add when you have proxy methods returning `Uni`/`Multi` or you want the native Vert.x event-loop integration. | + +Add exactly ONE of the two: + +```xml + + + xyz.juandiii + ark-quarkus-jackson + + + + + xyz.juandiii + ark-quarkus-jackson-vertx + +``` + +The vertx add-on transitively depends on the slim extension — you don't need both. + +--- + ## Installation ```xml @@ -17,14 +46,19 @@ The `ark-quarkus-jackson` extension auto-configures Ark HTTP clients for Quarkus ## What It Provides -The extension registers the following CDI beans: +The slim `ark-quarkus-jackson` extension registers the following CDI beans: | Bean | Scope | Description | |------|-------|-------------| | `JsonSerializer` | `@Singleton` | `JacksonClassicSerializer` using Quarkus-managed `ObjectMapper` | | `HttpTransport` | `@Singleton` | `ArkJdkSyncTransport` with default `HttpClient` | -| `MutinyHttpTransport` | `@Singleton` | `ArkVertxMutinyTransport` with Quarkus-managed `Vertx` | | `ArkClient.Builder` | `@Dependent` | Pre-configured sync builder | + +Add the `ark-quarkus-jackson-vertx` add-on to additionally get: + +| Bean | Scope | Description | +|------|-------|-------------| +| `MutinyHttpTransport` | `@Singleton` | `ArkVertxMutinyTransport` with Quarkus-managed `Vertx` | | `MutinyArkClient.Builder` | `@Dependent` | Pre-configured Mutiny builder | All beans use `@DefaultBean` - define your own to override any of them. From 812be7d3e2c77bab4256230a7eeb05ddc5ffb399 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Diego=20L=C3=B3pez?= Date: Tue, 30 Jun 2026 22:12:18 -0400 Subject: [PATCH 10/11] refactor(quarkus-jackson): remove dead quarkus-vertx-deployment from slim deployment pom Follow-up to plan 026 split. After the slim ArkProcessor stopped referencing any Vert.x classes (QuarkusVertxTlsResolver moved to the vertx add-on, Mutiny proxy provider reflection entries moved, Vert.x native resources moved), the quarkus-vertx-deployment dep in the slim deployment module is dead weight. Removing it keeps the slim extension's dep graph consistent with its 'JDK-only' promise. --- extensions/ark-quarkus-jackson/deployment/pom.xml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/extensions/ark-quarkus-jackson/deployment/pom.xml b/extensions/ark-quarkus-jackson/deployment/pom.xml index 878eebb..f0e2342 100644 --- a/extensions/ark-quarkus-jackson/deployment/pom.xml +++ b/extensions/ark-quarkus-jackson/deployment/pom.xml @@ -45,11 +45,6 @@ quarkus-arc-deployment - - io.quarkus - quarkus-vertx-deployment - - io.quarkus quarkus-jackson-deployment From 0c40fd26b5d85dc997ddd069e29d6a0170ae6142 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Diego=20L=C3=B3pez?= Date: Tue, 30 Jun 2026 22:12:18 -0400 Subject: [PATCH 11/11] chore(bom): add ark-quarkus-jackson-vertx artifacts to ark-bom Follow-up to plan 026 split. Adds two entries to ark-bom so users importing the BOM can pull the vertx add-on without specifying a version explicitly: - xyz.juandiii:ark-quarkus-jackson-vertx (runtime) - xyz.juandiii:ark-quarkus-jackson-vertx-deployment (deployment) Matches the existing pattern used for ark-quarkus-jackson + -deployment. --- core/ark-bom/pom.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/ark-bom/pom.xml b/core/ark-bom/pom.xml index 3c2dfc2..9d764d0 100644 --- a/core/ark-bom/pom.xml +++ b/core/ark-bom/pom.xml @@ -187,6 +187,16 @@ ark-quarkus-jackson-deployment 1.0.8-SNAPSHOT + + xyz.juandiii + ark-quarkus-jackson-vertx + 1.0.8-SNAPSHOT + + + xyz.juandiii + ark-quarkus-jackson-vertx-deployment + 1.0.8-SNAPSHOT + \ No newline at end of file