Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
7 changes: 4 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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<T>`. **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

Expand Down Expand Up @@ -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).
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.marketdata.sdk.internal;
package com.marketdata.sdk;

import java.io.IOException;
import java.nio.file.Files;
Expand All @@ -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}.
*
* <p>The only public construction path is {@link #loadFromProcess()}, which snapshots the live
* <p>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";
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
4 changes: 0 additions & 4 deletions src/main/java/com/marketdata/sdk/MarketDataClient.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package com.marketdata.sdk.internal;
package com.marketdata.sdk;

import org.jspecify.annotations.Nullable;

/**
* 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -7,7 +7,7 @@
* <p>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";

Expand Down
11 changes: 0 additions & 11 deletions src/main/java/com/marketdata/sdk/internal/package-info.java

This file was deleted.

11 changes: 9 additions & 2 deletions src/main/java/com/marketdata/sdk/package-info.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
/**
* Market Data Java SDK — public API entry point.
* Market Data Java SDK — public API root.
*
* <p>The entire public API is {@code @NullMarked}: every type, parameter, return, and field is
* <p>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.
*
* <p>{@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}.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.marketdata.sdk.internal;
package com.marketdata.sdk;

import static org.assertj.core.api.Assertions.assertThat;

Expand Down
1 change: 0 additions & 1 deletion src/test/java/com/marketdata/sdk/MarketDataClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.marketdata.sdk.internal;
package com.marketdata.sdk;

import static org.assertj.core.api.Assertions.assertThat;

Expand Down
Loading