Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
c8461ab
wipe src/ for clean-architecture restart
MarketDataDev03 May 15, 2026
f5e95cb
config cascade
MarketDataDev03 May 15, 2026
f2183ae
add tokens class
MarketDataDev03 May 15, 2026
7f49c94
add version class
MarketDataDev03 May 15, 2026
cf6c22c
adds RateLimitSnapshot & DemoMode
MarketDataDev03 May 15, 2026
59ea39a
adds MarketDataClient
MarketDataDev03 May 15, 2026
cd74c02
add Sealed exception hierarchy
MarketDataDev03 May 18, 2026
9ae56db
add asycnsemaphore
MarketDataDev03 May 18, 2026
fa2d20e
ADD retry pilicies
MarketDataDev03 May 18, 2026
52e9540
add HttpTransport
MarketDataDev03 May 18, 2026
422c87e
wire transport to MarketDataClient
MarketDataDev03 May 18, 2026
e5671d1
unversioned added
MarketDataDev03 May 18, 2026
5b9104b
Utilities added
MarketDataDev03 May 18, 2026
5b620a6
add Utilities.user()
MarketDataDev03 May 18, 2026
22189f8
add validateonstartup
MarketDataDev03 May 18, 2026
a1f0c1d
add utilities.status()
MarketDataDev03 May 18, 2026
b5eb2ba
status pre-check
MarketDataDev03 May 18, 2026
5817605
add retry-after
MarketDataDev03 May 18, 2026
8f9d618
add pre-fligth
MarketDataDev03 May 19, 2026
02906e3
add logger
MarketDataDev03 May 19, 2026
ffb0ba3
add date handling
MarketDataDev03 May 19, 2026
5abcda4
test added
MarketDataDev03 May 19, 2026
6ec5d87
runStartupValidation() skips demo mode
MarketDataDev03 May 19, 2026
63c87e5
runStartupValidation do not consume retry budget
MarketDataDev03 May 19, 2026
9333a9d
close() now dains waiters
MarketDataDev03 May 19, 2026
9190a13
HttpTransport ctrs reduce
MarketDataDev03 May 19, 2026
0cdc97a
MarketDataClient ctr rduce
MarketDataDev03 May 19, 2026
3f37461
optimize executeSync
MarketDataDev03 May 19, 2026
efccef1
add response object and methods
MarketDataDev03 May 19, 2026
e365bd1
parallel-arrays deserialization
MarketDataDev03 May 19, 2026
a55d517
avoid silence DotEnvLoader
MarketDataDev03 May 20, 2026
baf9400
improve url builder
MarketDataDev03 May 20, 2026
d975a47
improe rate limits
MarketDataDev03 May 20, 2026
08e9d73
improve http status mapper
MarketDataDev03 May 20, 2026
4243da4
use clock on checkRateLimitPreflight
MarketDataDev03 May 20, 2026
be1c104
bypass cache when /status
MarketDataDev03 May 20, 2026
0ef8528
remove unnecessary fqn
MarketDataDev03 May 20, 2026
a89cb7d
inmutable SDK logger
MarketDataDev03 May 20, 2026
e3d2228
do not expose direct env
MarketDataDev03 May 20, 2026
9d98442
obfuscate URI parameters
MarketDataDev03 May 20, 2026
eabbb8e
modify CLAUDE.md
MarketDataDev03 May 20, 2026
bfdc69b
after review
MarketDataDev03 May 20, 2026
ee3250d
preflight bypass
MarketDataDev03 May 20, 2026
e80c83d
ALLOWED_KEYS added to DotEnvLoader
MarketDataDev03 May 20, 2026
3f6b2dc
Loggers per-class consolidation
MarketDataDev03 May 20, 2026
6df7c8c
improve logger config
MarketDataDev03 May 20, 2026
4cb5a08
avoid harcoded jsonformatter
MarketDataDev03 May 20, 2026
3e68a6c
factory for ParallelArrays.listDeserializer
MarketDataDev03 May 20, 2026
2604844
add HttpTransport test
MarketDataDev03 May 20, 2026
a23b056
fixes
MarketDataDev03 May 20, 2026
886d11f
fixes
MarketDataDev03 May 20, 2026
dcc42be
fixes
MarketDataDev03 May 20, 2026
5aa3efe
add ishtml()
MarketDataDev03 May 20, 2026
71bc598
StatusCahce leak
MarketDataDev03 May 20, 2026
747099b
example added
MarketDataDev03 May 26, 2026
c4b350b
.env inline comments
MarketDataDev03 May 28, 2026
df3becd
add NPE validation to RequestHeaders
MarketDataDev03 May 28, 2026
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
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,16 @@ Thumbs.db
.env
.env.local

# Python (examples/mock-server)
.venv/
__pycache__/
*.pyc

# Logs / coverage
*.log
hs_err_pid*
replay_pid*


.claude/reviews/
.claude/prompts/
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Default retry attempts corrected from 3 to 4 (one initial + three retries) to
match SDK requirements Β§9.3 ("max 3 retries, yielding 4 total attempts").
- `.env` parser now strips trailing inline `# comment` markers (quote-aware: a
`#` inside single/double quotes or adjacent to value chars stays part of the
value). Previously a line like `MARKETDATA_TOKEN=abc # prod` produced the
literal value `abc # prod`, which passes `validateApiKey` (printable ASCII)
and surfaces later as a confusing `AuthenticationError` far from the .env
source that caused it.
- `RequestHeaders` canonical constructor now rejects a `null` `headers` map
with a clear `NullPointerException` naming the field, replacing the bare
`Map.copyOf(null)` NPE that left consumers hunting for the offending
argument. The wire-format deserializer additionally intercepts a top-level
JSON `null` body via `JsonDeserializer#getNullValue` and surfaces it as a
`ParseError` carrying the endpoint URL, status, and request id β€” preventing
a malformed `/headers/` response from manifesting as an opaque NPE further
down the call stack.

### Added
- Project scaffold per ADRs 001–007: Gradle Kotlin DSL build, JDK 17 toolchain,
Expand Down
30 changes: 13 additions & 17 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Repository state

This repo currently contains **documentation only** β€” no Java sources, no build scripts. Branch `00_base_setup` is the pre-implementation phase: all foundational technical decisions are being captured as ADRs *before* code lands. There is therefore nothing to build, lint, or test yet. When implementation starts, the build will be Gradle (Kotlin DSL) per ADR-003 β€” see "Locked-in tech stack" below.
This repo contains a working Java SDK in active development on branch `clean-architecture-restart`. Gradle 9.0 (Kotlin DSL) build per ADR-003; ~48 main + ~28 test source files; full CI matrix per ADR-002. The transport, retry, rate-limit, status-cache, and exception layers are wired; only the `utilities` resource faΓ§ade is implemented today β€” stocks/options/funds/markets resources are still to come (see "Deliberately deferred" below).

Sibling repo: `../api/` is the backend (Python/Django). The Python SDK lives at `../../sdk-py/` (referenced from ADRs). The cross-language `sdk-requirements.md` is referenced as `../sdk-requirements.md` from inside `docs/`; it is canonical but not committed in this repo.

Expand Down Expand Up @@ -62,14 +62,17 @@ 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 `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`).
- Β§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). `baseUrl` and `apiVersion` are normalized (trailing/leading slashes stripped) and validated (scheme http/https, host present, no query/fragment/user-info on `baseUrl`; `[A-Za-z0-9._-]+` on `apiVersion`) at resolution time, so misconfigured cascade inputs fail at construction with a clear message instead of producing malformed URLs later.
- Β§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`). `runStartupValidation` fires a single `GET /user/` (unversioned, like `/status/` and `/headers/`) via `utilities.validateAuth()` with `RetryPolicy.noRetry()`, skipping in demo mode; the no-retry policy keeps construction snappy on a slow/down API.
- Β§6 sealed `MarketDataException` hierarchy with the 7 canonical subtypes and full support context (`requestId`, `requestUrl`, `statusCode`, `timestamp`, `exceptionType`) + `getSupportInfo()`.
- Β§7 logging β€” `MarketDataLogging.configure(...)` installs `CanonicalLogFormatter` (the spec's `{timestamp} - {logger_name} - {level} - {message}` shape) on `com.marketdata.sdk` and applies the level from `MARKETDATA_LOGGING_LEVEL`. Detects consumer-pre-configured loggers (handler attached or level set) and backs off entirely so embedding the SDK doesn't clobber existing logging setups.
- Β§8 rate-limit tracking β€” `RateLimitHeaders.parse` reads the four `x-api-ratelimit-*` headers per response (all-or-nothing: a partial header set returns null rather than emitting a snapshot with phantom zeros); `HttpTransport.latestRateLimits` keeps the latest snapshot, exposed via `client.getRateLimits()`. Β§10.3 preflight (`HttpTransport.checkRateLimitPreflight`) fails fast on `remaining=0`, but only while `now < reset` β€” once the reset window elapses the request goes through so the next response refreshes the snapshot.
- Β§9 retry/backoff β€” `RetryPolicy` (4 total attempts = 1 initial + 3 retries, exponential 1sβ†’30s per Β§9.3) wired into `HttpTransport.executeAsync` via a per-attempt loop using `CompletableFuture.delayedExecutor` (no scheduled threads). Network errors and HTTP 501–599 retry; 500 and 4xx do not. Β§9.4 `Retry-After`: parsed from response headers via `RetryAfterHeader.parse` (supports both delta-seconds and HTTP-date), attached to `ServerError`, and honored by `RetryPolicy.backoffDelay` as an override of the calculated exponential delay. Β§9.5 `/status/` cache: `StatusCache` (stale-while-revalidate, 270s refresh / 300s expiry) gates retries on services reported offline; `HttpTransport.cacheAllowsRetry` has a self-referential bypass for `/status/` itself so the cache can never block its own refresh.
- Β§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 applied via `HttpRequest.Builder#timeout` in `HttpTransport.buildRequest`.
- Β§12 concurrency: 50-permit `AsyncSemaphore` on `HttpTransport` with acquire/release wired around every dispatch. The custom semaphore replaces `java.util.concurrent.Semaphore` so `executeAsync` never parks the caller's thread on a full pool (ADR-007).
- Β§9 retry/backoff: `RetryPolicy` (4 total attempts = 1 initial + 3 retries, exponential 1sβ†’30s per Β§9.3) wired into `HttpTransport.executeAsync` via a per-attempt loop using `CompletableFuture.delayedExecutor` (no scheduled threads). Network errors and HTTP 501–599 retry; 500 and 4xx do not.
- Β§13.5 response object β€” `Response<T>` wrapper exposes typed `data()`, `rawBody()` (defensive copy), `statusCode()`, `requestUrl()`, `requestId()`, format predicates (`isJson()`/`isCsv()`/`isHtml()`), `isNoData()`, and `saveToFile(Path)`. Every resource endpoint returns `Response<T>` so consumers get a uniform surface regardless of format.
- Β§15 packaging: SemVer, MIT `LICENSE`, `CHANGELOG.md` in Keep a Changelog format, version auto-detected via JAR manifest (`Implementation-Version`).
- Β§16 security: tokens never logged verbatim (use `Tokens.redact`); TLS validated by default (`HttpClient` does not expose a skip-verify option).
- Β§16 security: tokens never logged verbatim (use `Tokens.redact`); TLS validated by default (`HttpClient` does not expose a skip-verify option); query strings stripped from log output (logged as `/path?…`) so request params never persist to logs β€” exception context retains the full URI for diagnostic use; `EnvVars.systemLookup()` restricts reads to the declared `MARKETDATA_*` keys.
- ADR-002 CI: split into four workflows.
- `.github/workflows/pull-request.yml` β€” runs on PR `opened`/`synchronize`/`reopened` (no pre-PR push trigger by design). JDK 17 only. Runs `./gradlew build` (unit tests + Spotless + JaCoCo) and uploads coverage to Codecov. **Does not** run integration tests β€” those are handled by the on-demand workflow below.
- `.github/workflows/main.yml` β€” runs only on `push` to `main`. Two jobs: `verify` does the full forward-compat matrix `{17, 21, 25}` for unit tests via `-PtestJdk=N`; `integration-tests` does a parallel matrix `{17, 21, 25}` against the live API. Both are mandatory for the merge to be considered successful. The JDK 17 matrix entry of `verify` also uploads coverage to Codecov as the new baseline that PRs compare against. `integration-tests` fails the build if `MARKETDATA_TOKEN` secret is absent (it is required on main).
Expand All @@ -79,21 +82,14 @@ The Java SDK must also satisfy the canonical, cross-language [SDK Requirements](
- `-PtestJdk=N` is wired to **all** `Test` tasks (`test` and `integrationTest`) via `tasks.withType<Test>().configureEach { javaLauncher.set(...) }` in `build.gradle.kts`, so the matrix flag works uniformly across unit and integration tests.
- Coverage ratchet lives in `codecov.yml`: project status with `target: auto, threshold: 5%` (cannot drop >5 pp vs base branch) plus a patch-coverage requirement of 70 % on new code. Requires a `CODECOV_TOKEN` repo secret β€” without it the upload step fails because workflows pass `fail_ci_if_error: true`.

**Deliberately deferred (require the request/endpoint layer to land first):**
- Β§1.2 resource groupings (`client.stocks`, `client.options`, `client.funds`, `client.markets`, `client.utilities`).
- Β§2 endpoint method coverage; Β§3 universal parameters; Β§11 wire-format decoding.
- Β§5 actual `/user/` startup validation call (the `validateOnStartup` flag is the seam; the call itself comes with the request layer).
- Β§7 honoring `MARKETDATA_LOGGING_LEVEL` and the spec's exact `{timestamp} - {logger_name} - {level} - {message}` format. Currently the SDK uses `java.util.logging` with default formatting; consumers can attach their own handler.
- Β§8 rate-limit header parsing, pre-flight check, request-scoped attachment.
- Β§9 `/status/` cache workflow and `Retry-After` header override (retry/backoff itself lives in `RetryPolicy` and is wired; what is missing is the `/status/` pre-check before retrying 501–599 and respecting the server-specified `Retry-After` over the calculated exponential backoff).
- Β§13 100% coverage threshold via JaCoCo `violationRules`; deferred until there is functional code worth the threshold.
**Deliberately deferred (require the per-resource layer to land first):**
- Β§1.2 resource groupings β€” only `client.utilities()` is wired today; `client.stocks`, `client.options`, `client.funds`, `client.markets` still to come.
- Β§2 endpoint method coverage; Β§3 universal parameters; Β§11 wire-format decoding for resources beyond utilities. The plumbing (`ParallelArrays.zip` helper for the parallel-arrays shape, `JsonResponseParser`, `Response<T>` wrapper) is in place β€” each new endpoint just declares its fields and row builder.
- Β§8 request-scoped rate-limit attachment β€” today the snapshot is client-level (via `client.getRateLimits()`); attaching a per-response snapshot to `Response<T>` is a small follow-up when a consumer needs it.
- Β§13 100% coverage threshold via JaCoCo `violationRules`; deferred until the resource layer lands so the threshold meaningfully ratchets functional code, not just scaffolding.

When picking up new work, check this list before reaching for the SDK requirements doc β€” most foundational rules are already encoded in code; missing pieces are deferred deliberately, not by accident.

**Known latent gaps:**
- `HttpTransport.buildUri` URL-encodes query-param values with `URLEncoder.encode(..., UTF_8)`, which is form-encoding semantics: spaces become `+`, not `%20`. Fine for today's typed params (dates, numerics) but a future endpoint that takes an arbitrary string (e.g. `symbol="BRK A"`) would round-trip differently against an RFC-3986-strict server. Switch to a path/query-segment-aware encoder when the first such param lands. Tracked as Issue #10 of the 2026-05-11 review.
- `Retry-After` server header is parsed and respected by neither `RetryPolicy` nor `HttpTransport`. Today every retry uses the calculated exponential backoff (`min(1s Γ— 2^N, 30s)`). Implementing the override needs the response headers to reach `RetryPolicy.backoffDelay`, which today only sees the attempt index β€” most natural path is to surface a `Duration` on `ServerError` (or thread it through a separate channel) when 5xx responses carry the header. Follow-up of the Β§9 work.

## Acceptance checklist

`docs/java-sdk-requirements.md` ends with an "Acceptance Checklist" mapping each Java-specific requirements section to verifiable items. Treat it as the definition of done for v1: when implementing, work toward making each box checkable, and use it as a self-review pass before declaring a section complete.
92 changes: 92 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Convenience wrapper around ./gradlew and the consumer-test / mock-server.
# Run `make` (no args) or `make help` for the target list.

CONSUMER_DIR := examples/consumer-test
MOCK_DIR := examples/mock-server

.DEFAULT_GOAL := help

# ---------------------------------------------------------------------------
# Help
# ---------------------------------------------------------------------------

.PHONY: help
help: ## Show this help
@awk 'BEGIN {FS = ":.*##"; printf "Usage:\n make \033[36m<target>\033[0m\n\nTargets:\n"} \
/^# ===/ { in_section = 1; next } \
/^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-22s\033[0m %s\n", $$1, $$2 } \
/^## ---/ { printf "\n\033[33m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST)
@echo ""
@echo "Typical workflow:"
@echo " make publish # publish SDK to mavenLocal"
@echo " make mock-server # in another terminal"
@echo " make demo-config # in a third terminal"

# ---------------------------------------------------------------------------
## --- SDK build ---
# ---------------------------------------------------------------------------

.PHONY: build
build: ## Full build: unit tests + Spotless + JaCoCo (JDK 17)
./gradlew build

.PHONY: test
test: ## Unit tests only
./gradlew test

.PHONY: spotless
spotless: ## Apply code formatting
./gradlew spotlessApply

.PHONY: clean
clean: ## Clean all Gradle outputs (SDK + consumer-test)
./gradlew clean
cd $(CONSUMER_DIR) && ./gradlew clean

.PHONY: publish
publish: ## Publish SDK to ~/.m2 (prereq for any demo)
./gradlew publishToMavenLocal

# ---------------------------------------------------------------------------
## --- Mock server ---
# ---------------------------------------------------------------------------

.PHONY: mock-server
mock-server: ## Start the FastAPI mock server (blocks, Ctrl+C to stop)
cd $(MOCK_DIR) && ./run.sh

# ---------------------------------------------------------------------------
## --- Consumer demos (need `make publish` first) ---
# ---------------------------------------------------------------------------

.PHONY: demo-quickstart
demo-quickstart: ## Idiomatic per-resource usage tour (live API; grows as resources land)
cd $(CONSUMER_DIR) && ./gradlew runQuickstart

.PHONY: demo-live
demo-live: ## Live API smoke (needs MARKETDATA_TOKEN in examples/consumer-test/.env)
cd $(CONSUMER_DIR) && ./gradlew runLive

.PHONY: demo-config
demo-config: ## Demo mode, cascade, validation (needs mock-server)
cd $(CONSUMER_DIR) && ./gradlew runDemoConfig

.PHONY: demo-exceptions
demo-exceptions: ## Every MarketDataException subtype (needs mock-server)
cd $(CONSUMER_DIR) && ./gradlew runExceptions

.PHONY: demo-retry
demo-retry: ## Retry, Retry-After, preflight (needs mock-server)
cd $(CONSUMER_DIR) && ./gradlew runRetry

.PHONY: demo-response
demo-response: ## Response<T> surface features (needs mock-server)
cd $(CONSUMER_DIR) && ./gradlew runResponse

.PHONY: demo-concurrency
demo-concurrency: ## 50-permit semaphore (needs mock-server)
cd $(CONSUMER_DIR) && ./gradlew runConcurrency

.PHONY: demos-all
demos-all: ## Run every mock-server-based demo back-to-back (needs mock-server)
cd $(CONSUMER_DIR) && ./gradlew runDemoConfig runExceptions runRetry runResponse runConcurrency
4 changes: 4 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ tasks.register<JacocoReport>("jacocoAggregateReport") {
// main's last value) is enforced in CI β€” see .github/workflows/pull-request.yml
// and .github/scripts/check-coverage-delta.py. Not enforced locally so that
// dev iteration isn't blocked while coverage is in flux.
//
// SDK requirements Β§15.3 mandates 100% line coverage with explicit ignore
// comments on untestable lines. Target deferred until business resources land
// and the defensive-guards cleanup pass can run together.

spotless {
java {
Expand Down
Loading
Loading