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 f472527..4655755 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +
+

@@ -5,482 +7,316 @@

-

- 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+ -## The Problem +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. -Java has no shortage of HTTP clients. But each one forces a trade-off: +[![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) -- **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 +[**Quick Start**](#quick-start) โ€ข [**Frameworks**](#frameworks) โ€ข [**Execution Models**](#execution-models) โ€ข [**Architecture**](#architecture) โ€ข [**Documentation**](#documentation) -Ark gives you one client model that survives framework changes, transport changes, and execution-model changes. +
-## The Solution +
+Named after the ark - a vessel that carries safely across distance. +
-Ark separates the concerns that other clients bundle together: +
-| 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 | +## What is Ark? -One mental model. Any stack. No lock-in. +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. -## Why Ark? +**Core capabilities:** -- **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 +## Quick Start -Ark supports multiple ways to define HTTP clients. - -### Fluent API - -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; +**1. Add Ark** - import the BOM and a host starter (Spring shown; [other frameworks](#frameworks) below): -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). - ---- - -## 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 +The same model runs on every supported host - only the registration differs. --- -## 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` | +## Frameworks -Same fluent API - only the return type changes: +All coordinates under `groupId` **`xyz.juandiii`**, versioned by the [BOM](#quick-start). Pick a host: -```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. - -### Core + Jackson + JDK transport - -```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 - -``` - -For Spring WebFlux: +Slim (JDK transport only): ```xml - xyz.juandiii - ark-spring-boot-starter-webflux + xyz.juandiii + ark-quarkus-jackson ``` -For Quarkus (Jackson): +With Vert.x Mutiny transport (adds `Uni` / `Multi` proxy method support): ```xml - xyz.juandiii - ark-quarkus-jackson + xyz.juandiii + ark-quarkus-jackson-vertx ``` -Auto-configures `JsonSerializer` (Jackson 2.x), `ArkClient.Builder` (sync), and `MutinyArkClient.Builder` (reactive) as CDI beans. - ---- - -## 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. +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. -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: +
+Plain main() -- 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)`. +```java +Ark client = ArkClient.builder() + .serializer(new JacksonSerializer(new ObjectMapper())) + .transport(new ArkJdkSyncTransport(HttpClient.newHttpClient())) + .baseUrl("https://api.example.com") + .build(); +``` -For raw wire-level transport debugging, enable the underlying client's own logger: +`ark-core` has zero framework dependencies โ€” assemble it yourself. -- **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). 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..81bba74 100644 --- a/assets/logo/ark-logo.svg +++ b/assets/logo/ark-logo.svg @@ -1,38 +1,38 @@ - Ark - - - - - - + Ark + + + + + + - - - - + + + + - - - - - + + + + + - - - - - + + + + + - - - + + + - - ark + + ark 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 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. 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/runtime/src/main/java/xyz/juandiii/ark/quarkus/QuarkusVertxTlsResolver.java b/extensions/ark-quarkus-jackson-vertx/runtime/src/main/java/xyz/juandiii/ark/quarkus/vertx/QuarkusVertxTlsResolver.java similarity index 97% rename from extensions/ark-quarkus-jackson/runtime/src/main/java/xyz/juandiii/ark/quarkus/QuarkusVertxTlsResolver.java rename to extensions/ark-quarkus-jackson-vertx/runtime/src/main/java/xyz/juandiii/ark/quarkus/vertx/QuarkusVertxTlsResolver.java index 1e461e3..c02280e 100644 --- a/extensions/ark-quarkus-jackson/runtime/src/main/java/xyz/juandiii/ark/quarkus/QuarkusVertxTlsResolver.java +++ b/extensions/ark-quarkus-jackson-vertx/runtime/src/main/java/xyz/juandiii/ark/quarkus/vertx/QuarkusVertxTlsResolver.java @@ -1,4 +1,4 @@ -package xyz.juandiii.ark.quarkus; +package xyz.juandiii.ark.quarkus.vertx; import io.quarkus.tls.TlsConfiguration; import io.quarkus.tls.TlsConfigurationRegistry; @@ -39,4 +39,4 @@ private TlsConfiguration getTlsConfig(String 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-vertx/runtime/src/main/java/xyz/juandiii/ark/quarkus/vertx/VertxTlsResolver.java similarity index 90% rename from extensions/ark-quarkus-jackson/runtime/src/main/java/xyz/juandiii/ark/quarkus/VertxTlsResolver.java rename to extensions/ark-quarkus-jackson-vertx/runtime/src/main/java/xyz/juandiii/ark/quarkus/vertx/VertxTlsResolver.java index 534717c..0e2826f 100644 --- a/extensions/ark-quarkus-jackson/runtime/src/main/java/xyz/juandiii/ark/quarkus/VertxTlsResolver.java +++ b/extensions/ark-quarkus-jackson-vertx/runtime/src/main/java/xyz/juandiii/ark/quarkus/vertx/VertxTlsResolver.java @@ -1,4 +1,4 @@ -package xyz.juandiii.ark.quarkus; +package xyz.juandiii.ark.quarkus.vertx; import io.vertx.core.net.KeyCertOptions; import io.vertx.core.net.TrustOptions; @@ -15,4 +15,4 @@ public interface VertxTlsResolver { Optional resolveTrustOptions(String tlsConfigurationName); Optional resolveKeyCertOptions(String tlsConfigurationName); -} \ No newline at end of file +} 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/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 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..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 @@ -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; @@ -29,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() { @@ -40,8 +41,7 @@ AdditionalBeanBuildItem registerBeans() { return AdditionalBeanBuildItem.builder() .addBeanClasses( ArkProducer.class, - QuarkusTlsResolver.class, - QuarkusVertxTlsResolver.class + QuarkusTlsResolver.class ) .setUnremovable() .build(); @@ -50,9 +50,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 +83,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(); @@ -103,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", ""); @@ -117,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/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/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