From c815c85f3912e89a0300ac805a57b046e134db36 Mon Sep 17 00:00:00 2001 From: Lucas Giordano Date: Fri, 8 May 2026 16:22:08 -0300 Subject: [PATCH] implements single package arq --- CHANGELOG.md | 11 +++++++++-- CLAUDE.md | 7 ++++--- README.md | 5 +++-- .../marketdata/sdk/{internal => }/Configuration.java | 6 +++--- .../com/marketdata/sdk/{internal => }/EnvVars.java | 4 ++-- .../java/com/marketdata/sdk/MarketDataClient.java | 4 ---- .../com/marketdata/sdk/{internal => }/Tokens.java | 4 ++-- .../com/marketdata/sdk/{internal => }/Version.java | 4 ++-- .../com/marketdata/sdk/internal/package-info.java | 11 ----------- src/main/java/com/marketdata/sdk/package-info.java | 11 +++++++++-- .../sdk/{internal => }/ConfigurationTest.java | 2 +- .../java/com/marketdata/sdk/MarketDataClientTest.java | 1 - .../com/marketdata/sdk/{internal => }/TokensTest.java | 2 +- 13 files changed, 36 insertions(+), 36 deletions(-) rename src/main/java/com/marketdata/sdk/{internal => }/Configuration.java (95%) rename src/main/java/com/marketdata/sdk/{internal => }/EnvVars.java (92%) rename src/main/java/com/marketdata/sdk/{internal => }/Tokens.java (94%) rename src/main/java/com/marketdata/sdk/{internal => }/Version.java (89%) delete mode 100644 src/main/java/com/marketdata/sdk/internal/package-info.java rename src/test/java/com/marketdata/sdk/{internal => }/ConfigurationTest.java (99%) rename src/test/java/com/marketdata/sdk/{internal => }/TokensTest.java (96%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72cc98e..fbfd253 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- Project scaffold per ADRs 001–006: Gradle Kotlin DSL build, JDK 17 toolchain, +- Project scaffold per ADRs 001–007: Gradle Kotlin DSL build, JDK 17 toolchain, `integrationTest` source set, Spotless + JaCoCo, Vanniktech Maven Publish. - `MarketDataClient` skeleton with two public constructors — a no-arg one for production (everything resolved from the cascade) and a 4-arg one @@ -27,6 +27,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `RateLimits` record exposed via `MarketDataClient.getRateLimits()`. - JSpecify `@NullMarked` on every public package; JSpecify on `compileOnlyApi` so consumers get the annotations at compile time without a runtime dep. -- Token redaction utility (`internal.Tokens`) for log output. +- Token redaction utility (`Tokens`, package-private in the SDK root) for + log output. - MIT license; SDK version auto-detected from the JAR manifest (`Implementation-Version`). +- Single-package architecture per ADR-007: every infra class + (`Configuration`, `EnvVars`, `Tokens`, `Version`) lives in + `com.marketdata.sdk` as package-private. The `internal/` subpackage + was removed; the consumer's compiler cannot reference these types, + closing the "internal type leaks via constructor signature" gap that + every non-modular Java SDK has. diff --git a/CLAUDE.md b/CLAUDE.md index 970bc1f..877be32 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,7 +20,7 @@ The repo follows a strict **ADR-first** workflow: When asked to make architectural changes, prefer updating an existing ADR or proposing a new one (status `Proposed`) over silently editing requirements. -## Locked-in tech stack (ADRs 001–006, all Accepted) +## Locked-in tech stack (ADRs 001–007, all Accepted) These decisions are not up for debate without amending the corresponding ADR: @@ -31,6 +31,7 @@ These decisions are not up for debate without amending the corresponding ADR: - **`java.net.http.HttpClient` exclusively.** No third-party HTTP client (OkHttp, Apache) as a runtime dep — ever. HTTP/2 on (default). One shared `HttpClient` per `MarketDataClient`. Timeouts: 99s request, 2s connect. (ADR-004) - **Jackson (`jackson-databind`) for JSON.** Records-based response models (Jackson record support, 2.12+). The API's parallel-arrays wire format (e.g. `{"s":"ok","symbol":["AAPL","MSFT"],"price":[150.0,400.0]}`) is decoded via custom `JsonDeserializer` classes, *not* default reflection. Jackson is **not shaded** in v1; shading is held in reserve. (ADR-005) - **Sync + async parity per endpoint.** Every public endpoint exposes both `quote(...)` and `quoteAsync(...)`; async returns `CompletableFuture`. **Internal logic is async-first.** Sync methods are thin wrappers that call `.join()` and unwrap `CompletionException` to surface the underlying cause directly. Both surfaces share validation, retry, rate-limit, and concurrency-pool logic — no parallel implementations. Tests must cover both variants for every endpoint. (ADR-006) +- **Single-package internals.** Every infra and resource-façade class lives in `com.marketdata.sdk` (the root). The "internal" boundary is enforced by Java's package-private visibility — types not meant for consumers (`Configuration`, `EnvVars`, `Tokens`, `Version`, and the future `HttpTransport`, `RequestSpec`, `AsyncSemaphore`, etc.) drop the `public` modifier so the consumer's compiler simply cannot reference them. Resource façades (`MarketsResource`, etc.) stay `public final class` but with package-private constructors. Response DTOs and exceptions stay in their public subpackages (`com.marketdata.sdk.markets`, `com.marketdata.sdk.exception`); response records do not carry `@JsonDeserialize` annotations — wire-format deserializers register programmatically via a package-private Jackson `SimpleModule` on `HttpTransport`'s `ObjectMapper`. (ADR-007) ## Kotlin-interop rules for the public API @@ -61,8 +62,8 @@ The Java SDK must also satisfy the canonical, cross-language [SDK Requirements]( **Already wired in:** - §1.1 client object — `MarketDataClient` with two public constructors: a no-arg one for production (everything from the cascade) and a 4-arg `(apiKey, baseUrl, apiVersion, validateOnStartup)` for tests and short-lived runtimes. All fields `final` (immutable). Default base URL `https://api.marketdata.app`, default API version `v1`, single shared `HttpClient`, `User-Agent: marketdata-sdk-java/{version}` (version auto-detected from JAR manifest), `close()` for resource release, `getRateLimits()` accessor. -- §4 configuration cascade — `Configuration.resolve(...)` does explicit → `MARKETDATA_*` env var → `.env` in CWD → default. Env var names live in `internal.EnvVars`. The 4-arg constructor's parameters feed step 1; the no-arg constructor skips it and starts at step 2. -- §5 demo mode + `validateOnStartup` parameter on the 4-arg constructor (defaults to `true` via the no-arg constructor); token redaction via `internal.Tokens.redact` (matches the spec example `***…***YKT0`). +- §4 configuration cascade — `Configuration.resolve(...)` does explicit → `MARKETDATA_*` env var → `.env` in CWD → default. Env var names live in `EnvVars` (package-private, in the SDK root package). The 4-arg constructor's parameters feed step 1; the no-arg constructor skips it and starts at step 2. +- §5 demo mode + `validateOnStartup` parameter on the 4-arg constructor (defaults to `true` via the no-arg constructor); token redaction via `Tokens.redact` (matches the spec example `***…***YKT0`). - §6 sealed `MarketDataException` hierarchy with the 7 canonical subtypes and full support context (`requestId`, `requestUrl`, `statusCode`, `timestamp`, `exceptionType`) + `getSupportInfo()`. - §10 timeouts: `REQUEST_TIMEOUT = 99s` and `CONNECT_TIMEOUT = 2s` exposed as constants on `MarketDataClient`. Connect timeout is wired into the `HttpClient`; the per-request 99 s timeout is a constant ready to be applied to `HttpRequest.Builder#timeout` when the request layer lands. - §12 concurrency: `Semaphore(50)` field on `MarketDataClient` (wiring of acquire/release lands with the request layer). diff --git a/README.md b/README.md index 0c286b1..7d8bfd5 100644 --- a/README.md +++ b/README.md @@ -125,9 +125,10 @@ MARKETDATA_RUN_INTEGRATION_TESTS=true ./gradlew integrationTest ## Package layout ``` -com.marketdata.sdk # MarketDataClient, RateLimits (public surface) +com.marketdata.sdk # MarketDataClient + RateLimits (public); + # Configuration, EnvVars, Tokens, Version + # are package-private and not part of the API com.marketdata.sdk.exception # Sealed MarketDataException hierarchy + ErrorContext -com.marketdata.sdk.internal # Tokens, EnvVars, Configuration, Version (do not depend on) ``` Every public package is `@NullMarked` (JSpecify): non-null is the default; diff --git a/src/main/java/com/marketdata/sdk/internal/Configuration.java b/src/main/java/com/marketdata/sdk/Configuration.java similarity index 95% rename from src/main/java/com/marketdata/sdk/internal/Configuration.java rename to src/main/java/com/marketdata/sdk/Configuration.java index 675dff6..a2dbc6f 100644 --- a/src/main/java/com/marketdata/sdk/internal/Configuration.java +++ b/src/main/java/com/marketdata/sdk/Configuration.java @@ -1,4 +1,4 @@ -package com.marketdata.sdk.internal; +package com.marketdata.sdk; import java.io.IOException; import java.nio.file.Files; @@ -12,13 +12,13 @@ * Resolves SDK configuration values per the cascade in SDK requirements §4: {@code explicit value → * MARKETDATA_* env var → .env file in CWD → built-in default}. * - *

The only public construction path is {@link #loadFromProcess()}, which snapshots the live + *

The single canonical construction path is {@link #loadFromProcess()}, which snapshots the live * environment and the {@code .env} file once. The constructor is strictly private — there is no * production-callable backdoor for injecting arbitrary maps. Tests reach the private constructor * via reflection (see {@code ConfigurationTest}); this is by design so a developer can't * accidentally take a shortcut around the canonical load path. */ -public final class Configuration { +final class Configuration { public static final String DEFAULT_BASE_URL = "https://api.marketdata.app"; public static final String DEFAULT_API_VERSION = "v1"; diff --git a/src/main/java/com/marketdata/sdk/internal/EnvVars.java b/src/main/java/com/marketdata/sdk/EnvVars.java similarity index 92% rename from src/main/java/com/marketdata/sdk/internal/EnvVars.java rename to src/main/java/com/marketdata/sdk/EnvVars.java index 16f7e39..e7ba768 100644 --- a/src/main/java/com/marketdata/sdk/internal/EnvVars.java +++ b/src/main/java/com/marketdata/sdk/EnvVars.java @@ -1,10 +1,10 @@ -package com.marketdata.sdk.internal; +package com.marketdata.sdk; /** * Names of the {@code MARKETDATA_*} environment variables consulted by the SDK. Mirrors SDK * requirements §4. */ -public final class EnvVars { +final class EnvVars { public static final String TOKEN = "MARKETDATA_TOKEN"; public static final String BASE_URL = "MARKETDATA_BASE_URL"; diff --git a/src/main/java/com/marketdata/sdk/MarketDataClient.java b/src/main/java/com/marketdata/sdk/MarketDataClient.java index c2410d5..4447806 100644 --- a/src/main/java/com/marketdata/sdk/MarketDataClient.java +++ b/src/main/java/com/marketdata/sdk/MarketDataClient.java @@ -1,9 +1,5 @@ package com.marketdata.sdk; -import com.marketdata.sdk.internal.Configuration; -import com.marketdata.sdk.internal.EnvVars; -import com.marketdata.sdk.internal.Tokens; -import com.marketdata.sdk.internal.Version; import java.net.http.HttpClient; import java.time.Duration; import java.util.concurrent.Semaphore; diff --git a/src/main/java/com/marketdata/sdk/internal/Tokens.java b/src/main/java/com/marketdata/sdk/Tokens.java similarity index 94% rename from src/main/java/com/marketdata/sdk/internal/Tokens.java rename to src/main/java/com/marketdata/sdk/Tokens.java index b0cd604..a17948a 100644 --- a/src/main/java/com/marketdata/sdk/internal/Tokens.java +++ b/src/main/java/com/marketdata/sdk/Tokens.java @@ -1,4 +1,4 @@ -package com.marketdata.sdk.internal; +package com.marketdata.sdk; import org.jspecify.annotations.Nullable; @@ -6,7 +6,7 @@ * Token redaction helpers. SDK requirements §5 / §16: API tokens must never appear in log output * verbatim. */ -public final class Tokens { +final class Tokens { /** * Minimum number of asterisks emitted before the trailing 4 chars, matching the SDK requirements diff --git a/src/main/java/com/marketdata/sdk/internal/Version.java b/src/main/java/com/marketdata/sdk/Version.java similarity index 89% rename from src/main/java/com/marketdata/sdk/internal/Version.java rename to src/main/java/com/marketdata/sdk/Version.java index 3f9746a..db0241a 100644 --- a/src/main/java/com/marketdata/sdk/internal/Version.java +++ b/src/main/java/com/marketdata/sdk/Version.java @@ -1,4 +1,4 @@ -package com.marketdata.sdk.internal; +package com.marketdata.sdk; /** * Reads the SDK's version from the JAR manifest's {@code Implementation-Version} attribute (SDK @@ -7,7 +7,7 @@ *

Falls back to {@code "0.0.0-dev"} when the class is not loaded from a JAR (e.g. running tests * from class files). */ -public final class Version { +final class Version { private static final String FALLBACK = "0.0.0-dev"; diff --git a/src/main/java/com/marketdata/sdk/internal/package-info.java b/src/main/java/com/marketdata/sdk/internal/package-info.java deleted file mode 100644 index 8f8cf2f..0000000 --- a/src/main/java/com/marketdata/sdk/internal/package-info.java +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Internal SDK utilities. Not part of the public API surface — types and methods here may change in - * any release without notice. - * - *

Java has no enforceable "internal" visibility across packages, so this boundary is convention - * only. - */ -@NullMarked -package com.marketdata.sdk.internal; - -import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/com/marketdata/sdk/package-info.java b/src/main/java/com/marketdata/sdk/package-info.java index b23a077..cac5c4e 100644 --- a/src/main/java/com/marketdata/sdk/package-info.java +++ b/src/main/java/com/marketdata/sdk/package-info.java @@ -1,7 +1,14 @@ /** - * Market Data Java SDK — public API entry point. + * Market Data Java SDK — public API root. * - *

The entire public API is {@code @NullMarked}: every type, parameter, return, and field is + *

This package hosts both the public API surface ({@link com.marketdata.sdk.MarketDataClient}, + * {@link com.marketdata.sdk.RateLimits}, and the resource façades) and every package-private + * internal class (configuration cascade, env-var keys, token redaction, version detection, and the + * HTTP/wire-format infrastructure). Per ADR-007, the "internal" boundary is enforced by Java's + * package-private visibility: types not meant for consumers omit the {@code public} modifier so the + * consumer's compiler simply cannot reference them. + * + *

{@code @NullMarked} applies at the package level — every type, parameter, return, and field is * non-null by default. Mark nullable items explicitly with {@link * org.jspecify.annotations.Nullable}. */ diff --git a/src/test/java/com/marketdata/sdk/internal/ConfigurationTest.java b/src/test/java/com/marketdata/sdk/ConfigurationTest.java similarity index 99% rename from src/test/java/com/marketdata/sdk/internal/ConfigurationTest.java rename to src/test/java/com/marketdata/sdk/ConfigurationTest.java index f67093f..7151ec1 100644 --- a/src/test/java/com/marketdata/sdk/internal/ConfigurationTest.java +++ b/src/test/java/com/marketdata/sdk/ConfigurationTest.java @@ -1,4 +1,4 @@ -package com.marketdata.sdk.internal; +package com.marketdata.sdk; import static org.assertj.core.api.Assertions.assertThat; diff --git a/src/test/java/com/marketdata/sdk/MarketDataClientTest.java b/src/test/java/com/marketdata/sdk/MarketDataClientTest.java index 24f0ddf..40b6fd5 100644 --- a/src/test/java/com/marketdata/sdk/MarketDataClientTest.java +++ b/src/test/java/com/marketdata/sdk/MarketDataClientTest.java @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.assertThat; -import com.marketdata.sdk.internal.Configuration; import org.junit.jupiter.api.Test; class MarketDataClientTest { diff --git a/src/test/java/com/marketdata/sdk/internal/TokensTest.java b/src/test/java/com/marketdata/sdk/TokensTest.java similarity index 96% rename from src/test/java/com/marketdata/sdk/internal/TokensTest.java rename to src/test/java/com/marketdata/sdk/TokensTest.java index b4d183f..4be730c 100644 --- a/src/test/java/com/marketdata/sdk/internal/TokensTest.java +++ b/src/test/java/com/marketdata/sdk/TokensTest.java @@ -1,4 +1,4 @@ -package com.marketdata.sdk.internal; +package com.marketdata.sdk; import static org.assertj.core.api.Assertions.assertThat;