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
92 changes: 53 additions & 39 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ help: ## Show this help
@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"
@echo " make example-stocks # a resource example (live API, needs a token)"
@echo " make mock-server # in another terminal, then:"
@echo " make example-concurrency # a cross-cutting example (needs the mock server)"

# ---------------------------------------------------------------------------
## --- SDK build ---
Expand Down Expand Up @@ -56,53 +57,66 @@ mock-server: ## Start the FastAPI mock server (blocks, Ctrl+C to stop)
cd $(MOCK_DIR) && ./run.sh

# ---------------------------------------------------------------------------
## --- Consumer demos (need `make publish` first) ---
## --- Examples (need `make publish` first) ---
# ---------------------------------------------------------------------------
# Resource examples hit the LIVE API (need a token). The cross-cutting examples that show otherwise-
# invisible behavior (concurrency, retry, errors) drive the mock server — run `make mock-server`
# first for those. `make example-list` prints every runnable example with a one-line description.

.PHONY: demo-quickstart
demo-quickstart: ## Idiomatic per-resource usage tour (live API; grows as resources land)
cd $(CONSUMER_DIR) && ./gradlew runQuickstart
.PHONY: example-list
example-list: ## List every runnable example with a description
cd $(CONSUMER_DIR) && ./gradlew tasks --group examples

.PHONY: demo-live
demo-live: ## Live API smoke (needs MARKETDATA_TOKEN in examples/consumer-test/.env)
cd $(CONSUMER_DIR) && ./gradlew runLive
# --- resource examples (live API) ---
.PHONY: example-utilities
example-utilities: ## utilities: health, quota, request echo (live)
cd $(CONSUMER_DIR) && ./gradlew runUtilities

.PHONY: demo-config
demo-config: ## Demo mode, cascade, validation (needs mock-server)
cd $(CONSUMER_DIR) && ./gradlew runDemoConfig
.PHONY: example-stocks
example-stocks: ## stocks: candles, quote, batch quotes (live)
cd $(CONSUMER_DIR) && ./gradlew runStocks

.PHONY: demo-exceptions
demo-exceptions: ## Every MarketDataException subtype (needs mock-server)
cd $(CONSUMER_DIR) && ./gradlew runExceptions
.PHONY: example-options
example-options: ## options: lookup, expirations, chain, quote (live)
cd $(CONSUMER_DIR) && ./gradlew runOptions

.PHONY: demo-retry
demo-retry: ## Retry, Retry-After, preflight (needs mock-server)
cd $(CONSUMER_DIR) && ./gradlew runRetry
.PHONY: example-funds
example-funds: ## funds: NAV candles (live)
cd $(CONSUMER_DIR) && ./gradlew runFunds

.PHONY: demo-response
demo-response: ## MarketDataResponse surface features (needs mock-server)
cd $(CONSUMER_DIR) && ./gradlew runResponse
.PHONY: example-markets
example-markets: ## markets: open/closed calendar (live)
cd $(CONSUMER_DIR) && ./gradlew runMarkets

.PHONY: demo-concurrency
demo-concurrency: ## 50-permit semaphore (needs mock-server)
cd $(CONSUMER_DIR) && ./gradlew runConcurrency
.PHONY: example-kotlin
example-kotlin: ## the same SDK from Kotlin: sync + async (live)
cd $(CONSUMER_DIR) && ./gradlew runKotlinQuickstart

.PHONY: demo-options
demo-options: ## Full options surface: every endpoint + all params, CSV facet, columns, Option A (needs mock-server)
cd $(CONSUMER_DIR) && ./gradlew runOptions
# --- cross-cutting examples ---
.PHONY: example-sync-async
example-sync-async: ## sync vs async, parallel fan-out (live)
cd $(CONSUMER_DIR) && ./gradlew runSyncVsAsync

.PHONY: demo-stocks
demo-stocks: ## Full stocks surface: every endpoint + all params, CSV facet, columns, Option A (needs mock-server)
cd $(CONSUMER_DIR) && ./gradlew runStocks
.PHONY: example-config
example-config: ## constructors, cascade, redaction, validation (offline)
cd $(CONSUMER_DIR) && ./gradlew runConfiguration

.PHONY: demo-markets
demo-markets: ## Full markets surface: status + all params (open/closed calendar, null cells), CSV facet, columns, Option A (needs mock-server)
cd $(CONSUMER_DIR) && ./gradlew runMarkets
.PHONY: example-response
example-response: ## response wrapper: data, metadata, formats, saveToFile (live)
cd $(CONSUMER_DIR) && ./gradlew runResponseFormats

.PHONY: demo-funds
demo-funds: ## Full funds surface: candles + all params (no volume/intraday/chunking), CSV facet, columns, Option A (needs mock-server)
cd $(CONSUMER_DIR) && ./gradlew runFunds
.PHONY: example-concurrency
example-concurrency: ## fan-out async + 50-permit cap, observed (needs mock-server)
cd $(CONSUMER_DIR) && ./gradlew runConcurrency

.PHONY: example-retry
example-retry: ## automatic retry + backoff + Retry-After (needs mock-server)
cd $(CONSUMER_DIR) && ./gradlew runRetry

.PHONY: example-errors
example-errors: ## the sealed exception hierarchy and how to handle it (needs mock-server)
cd $(CONSUMER_DIR) && ./gradlew runErrors

.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 runOptions runStocks runMarkets runFunds
.PHONY: examples-mock
examples-mock: ## Run the three mock-server examples back-to-back (needs mock-server)
cd $(CONSUMER_DIR) && ./gradlew runConcurrency runRetry runErrors
153 changes: 61 additions & 92 deletions examples/consumer-test/README.md
Original file line number Diff line number Diff line change
@@ -1,112 +1,81 @@
# consumer-test
# Examples

A collection of small runnable apps that exercise every consumer-facing
behavior of `marketdata-sdk-java`. Each app stands alone — pick the one that
matches the scenario you want to see, run it, read the console output.
Small, self-contained programs that show how to use `marketdata-sdk-java`. Each one is meant to be
**read and copied** — pick the file that matches what you want to do, run it, look at the output.

Lives under `examples/` rather than as a `src/test` source set on purpose: it
consumes the SDK as an *external* artifact (via `mavenLocal`), so the demos
exercise exactly the shape a published JAR exposes — no accidental package-
private leaks, no access to internal seams.
They consume the SDK as a published artifact (via `mavenLocal`), exactly as your own project would,
so what you see is the real public API.

## One-time setup
## Layout

```
src/main/java/com/marketdata/examples/
resources/ one file per resource — the calls you'd write first, plus an "Advanced" file each
common/ cross-cutting topics that apply to every resource (do these once, not per resource)
src/main/kotlin/com/marketdata/examples/
Quickstart.kt the same SDK, from Kotlin
```

### Resource examples (live API)

| Example | Shows |
|---|---|
| `UtilitiesExample` | API health, your account quota, request echo |
| `StocksExample` | candles, a quote, a multi-symbol batch quote |
| `OptionsExample` | symbol lookup, expirations, a filtered chain, a contract quote |
| `FundsExample` | mutual-fund NAV candles |
| `MarketsExample` | the exchange open/closed calendar |
| `…AdvancedExample` (one per resource) | universal params, date windows, column projection, CSV output, sealed chain filters |

### Cross-cutting examples (`common/`)

| Example | Shows | Needs |
|---|---|---|
| `SyncVsAsyncExample` | the sync call vs its async (`CompletableFuture`) variant, and a parallel fan-out | live API |
| `ConfigurationExample` | constructors, the config cascade, token redaction, fail-fast validation | offline |
| `ResponseFormatsExample` | the response wrapper: typed data, metadata, format predicates, raw body, `saveToFile` | live API |
| `ConcurrentRequestsExample` | firing many requests at once and the SDK's 50-request concurrency cap | mock server |
| `RetryAndBackoffExample` | automatic retry, exponential backoff, `Retry-After` | mock server |
| `ErrorHandlingExample` | the sealed `MarketDataException` hierarchy and how to branch on it | mock server |

The cross-cutting examples cover these behaviors **once**, using whichever resource is handy as the
vehicle — they're not repeated per resource.

## Running

```bash
# 1. Publish the SDK to your local Maven cache. Run from the SDK root
# (two directories up). The Makefile wraps it:
cd ../..
# 1. Publish the SDK to your local Maven cache (run once, from the SDK root, two dirs up):
make publish

# 2. (For runLive only) put your token in this directory's .env:
# 2. For the live examples, put your token where the SDK's cascade can find it:
echo "MARKETDATA_TOKEN=your-token-here" > examples/consumer-test/.env
```

## Running

From the SDK root, the easy path is `make` (see `make help` for the full list):
Then run any example by its Gradle task (from this directory) or its `make` target (from the SDK
root):

```bash
make demo-quickstart # idiomatic per-resource tour — live API, no mock
make demo-live # full plumbing smoke — live API, no mock
make demo-config # config, validation, demo mode — needs mock server
make demo-exceptions # every MarketDataException subtype — needs mock server
make demo-retry # retry, Retry-After, preflight — needs mock server
make demo-response # Response<T> features — needs mock server
make demo-concurrency # 50-permit semaphore — needs mock server
make demos-all # the five mock-server demos back-to-back
./gradlew runStocks # or: make example-stocks
./gradlew runSyncVsAsync # or: make example-sync-async
./gradlew tasks --group examples # list them all (make example-list)
```

Or directly from this directory, bypassing the Makefile:
`UtilitiesExample`'s `status()` call, `SyncVsAsyncExample`, and `ResponseFormatsExample` use a public
endpoint and run **without** a token. The other resource examples need one (and options/funds data
needs the matching entitlements); without it they print a one-line hint instead of crashing.

```bash
./gradlew tasks --group "consumer demos" # list all apps
./gradlew runLive # same as `make demo-live`
./gradlew runDemoConfig # etc.
```
## The mock server (for the three behavior examples)

Apps that say "needs mock server" require the mock running in another
terminal. Easiest:
`ConcurrentRequestsExample`, `RetryAndBackoffExample` and `ErrorHandlingExample` demonstrate things
you can't see against the live API — the concurrency cap, deterministic retry timing, each error
type on demand. They point the SDK at a local mock server whose responses are scripted up front.
Start it in another terminal first:

```bash
make mock-server # from the SDK root
# or, equivalently:
cd ../mock-server && ./run.sh
make mock-server # from the SDK root
# or: cd ../mock-server && ./run.sh
```

Without it the demo fails fast with a clear "server not reachable" message.

## What each app shows

| App | Scenario | What you should see |
|---|---|---|
| **QuickstartApp** | Idiomatic per-resource usage. Designed to **grow** — each new SDK resource adds a section. Start here. | For each wired resource, one short snippet per typical call + console output of the typed data the consumer would actually use. Today: `utilities` (status / user / headers) and `options` (lookup / expirations / strikes / chain incl. sealed filters + `expiration=all` + `rho` / quote / quotes with `countback`). |
| **LiveSmokeApp** | The happy path against the real API | client.toString redacted; sync + async parity on status/user/headers; parallel calls completing in ≈ slowest-single-call wall-time; final rate-limit snapshot populated |
| **DemoAndConfigApp** | Construction-time behavior | demo-mode skip; §16 token redaction (short ≤8 → full; >8 → ***…***ABCD); cascade (explicit wins); IAE on invalid baseUrl / CRLF API key; validateOnStartup 200 vs 401 paths |
| **ExceptionsApp** | Every sealed exception subtype | 401 → AuthenticationError; 400 → BadRequestError; 429 → RateLimitError (+ Retry-After); 500 → ServerError (no retry); 503×4 → ServerError after ≈7s; malformed JSON → ParseError; empty body → ParseError (§29 fix message); connection refused → NetworkError after retries; ADR-002 sealed routing via instanceof |
| **RetryBehaviorApp** | The §9 retry contract | 503→503→200 recovers in ≈3s; Retry-After delta overrides exponential; Retry-After HTTP-date honored; pathological Retry-After (1 day) capped at 10 min — falls back to exponential; §10.3 preflight blocks the 2nd request when snapshot reports remaining=0 (0ms wall-time, 0 server-side requests) |
| **ResponseFeaturesApp** | §13.5 Response<T> surface | isJson / isCsv / isHtml mutually exclusive; 404 + `{"s":"no_data"}` returns successfully with isNoData=true; rawBody() is a defensive copy (consumer mutations don't leak); saveToFile() writes verbatim; toString() omits data + redacts query (§16) |
| **ConcurrencyApp** | §12 / ADR-007 50-permit semaphore | 60 parallel calls; server observes peak in-flight = exactly 50; total wall-time ≈ 2× per-call delay (two batches of 50+10) |

## Adding a section to QuickstartApp

`QuickstartApp` is the only app in this directory designed to be **extended** as
the SDK grows. The other six prove a fixed contract; this one is the running
catalog of "what each resource looks like in consumer code".

When a new resource lands on the SDK:

1. Open `src/main/java/com/marketdata/consumer/QuickstartApp.java`.
2. Uncomment the matching placeholder line in `main(...)` (e.g.
`// stocksExamples(client);`).
3. Implement `xxxExamples(MarketDataClient client)` following the
`utilitiesExamples` shape: one `Console.step` + one short SDK call + one
`Console.ok` per typical use case. Catch `AuthenticationError` separately
when the endpoint needs a token, so the demo stays runnable in demo mode.
4. Keep each example to **3–5 lines of SDK code** — the goal is "what you'd
copy-paste into your own app", not exhaustive coverage. Edge cases belong
in the other demos.

## How the mock server fits

The mock server (FastAPI, `../mock-server/`) is what makes the
non-live demos deterministic. Each demo POSTs a list of scripted responses
to `/_admin/script`, then makes its real SDK call against the same host —
the server's catch-all pops exactly the scripted response. Apps that need to
see specific status codes, headers (Retry-After, x-api-ratelimit-*), or
timing behavior depend on this scripting.

The control plane (`/_admin/*`) is hit with a plain `java.net.http.HttpClient`,
not the SDK — keeping the SDK's surface uncluttered.

> Note: `MockServerControl` forces HTTP/1.1 because uvicorn's HTTP/2 upgrade
> handling drops POST bodies during the negotiation. The SDK itself stays on
> its ADR-004 HTTP/2 default — only the admin client downgrades.

## Caveats

- The local `.env` has a token that's been used during development. If the
token is no longer valid against api.marketdata.app, `runLive` will show
`AuthenticationError` on calls that need it (status/ stays public).
- "Demo mode" (no token at all) is hard to see if you have a token in your
env or `.env` — the cascade picks it up. The demo detects this and prints
a skip note rather than constructing a misleading client.
Without it those three examples fail fast with a clear "mock server not reachable" message. The mock
is a teaching aid (`com.marketdata.examples.util.MockServer`) — it is **not** part of the SDK and you
never need it in your own code.
Loading
Loading