Skip to content

Commit fa6b5fc

Browse files
change from builder to constructor
1 parent 2bd1bf5 commit fa6b5fc

6 files changed

Lines changed: 101 additions & 86 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111
- Project scaffold per ADRs 001–006: Gradle Kotlin DSL build, JDK 17 toolchain,
1212
`integrationTest` source set, Spotless + JaCoCo, Vanniktech Maven Publish.
13-
- `MarketDataClient` skeleton with builder, default base URL
14-
(`https://api.marketdata.app`), default API version (`v1`), 99 s request /
15-
2 s connect timeouts, HTTP/2, demo mode, `validateOnStartup` toggle, and a
16-
50-permit concurrency semaphore (wiring lands with the request layer).
17-
- Configuration cascade: explicit builder values → `MARKETDATA_*` environment
18-
variables → `.env` file in CWD → built-in defaults.
13+
- `MarketDataClient` skeleton with two public constructors — a no-arg one
14+
for production (everything resolved from the cascade) and a 4-arg one
15+
(`apiKey`, `baseUrl`, `apiVersion`, `validateOnStartup`) for tests and
16+
short-lived runtimes. Default base URL (`https://api.marketdata.app`),
17+
default API version (`v1`), 99 s request / 2 s connect timeouts, HTTP/2,
18+
demo mode, `validateOnStartup` toggle, and a 50-permit concurrency
19+
semaphore (wiring lands with the request layer).
20+
- Configuration cascade: explicit constructor parameters → `MARKETDATA_*`
21+
environment variables → `.env` file in CWD → built-in defaults.
1922
- Sealed `MarketDataException` hierarchy with the seven canonical subtypes
2023
(`AuthenticationError`, `BadRequestError`, `NotFoundError`, `RateLimitError`,
2124
`ServerError`, `NetworkError`, `ParseError`), each carrying support context

CLAUDE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@ If you find yourself reaching for Lombok, AutoValue, or an abstract-base excepti
6060
The Java SDK must also satisfy the canonical, cross-language [SDK Requirements](https://www.marketdata.app/docs/sdk/sdk-requirements/) (referenced from inside the ADRs as `../sdk-requirements.md`). The current scaffold applies the **foundational** rules from that doc; per-endpoint and per-request rules land alongside the request layer. Specifically:
6161

6262
**Already wired in:**
63-
- §1.1 client object — `MarketDataClient` builder, 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.
64-
- §4 configuration cascade — `Configuration.resolve(...)` does explicit → `MARKETDATA_*` env var → `.env` in CWD → default. Env var names live in `internal.EnvVars`.
65-
- §5 demo mode + `validateOnStartup` toggle on the builder; token redaction via `internal.Tokens.redact` (matches the spec example `***…***YKT0`).
63+
- §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.
64+
- §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.
65+
- §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`).
6666
- §6 sealed `MarketDataException` hierarchy with the 7 canonical subtypes and full support context (`requestId`, `requestUrl`, `statusCode`, `timestamp`, `exceptionType`) + `getSupportInfo()`.
6767
- §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.
6868
- §12 concurrency: `Semaphore(50)` field on `MarketDataClient` (wiring of acquire/release lands with the request layer).

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,15 @@ common path is two lines (per SDK requirements §"Easy Default Requests"):
3232
### Java
3333

3434
```java
35-
try (var client = MarketDataClient.builder().build()) {
35+
try (var client = new MarketDataClient()) {
3636
// endpoint methods land in subsequent iterations
3737
}
3838
```
3939

4040
### Kotlin
4141

4242
```kotlin
43-
MarketDataClient.builder().build().use { client ->
43+
MarketDataClient().use { client ->
4444
// endpoint methods land in subsequent iterations
4545
}
4646
```
@@ -50,7 +50,9 @@ MarketDataClient.builder().build().use { client ->
5050
Values are resolved through this cascade (highest priority first), per
5151
SDK requirements §4:
5252

53-
1. Explicit builder methods — `apiKey(...)`, `baseUrl(...)`, `apiVersion(...)`
53+
1. Explicit constructor parameters — `apiKey`, `baseUrl`, `apiVersion`
54+
(passed to `new MarketDataClient(apiKey, baseUrl, apiVersion, validateOnStartup)`;
55+
the no-arg `new MarketDataClient()` skips this step and starts at #2)
5456
2. Environment variables (table below)
5557
3. `.env` file in the current working directory
5658
4. Built-in defaults

docs/java-sdk-requirements.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,12 @@ The README and per-method docs must include at least one Kotlin usage
118118
example alongside the Java example for the quick-start path:
119119

120120
```kotlin
121-
val client = MarketDataClient.builder()
122-
.apiKey("KEY")
123-
.build()
121+
val client = MarketDataClient(
122+
apiKey = "KEY",
123+
baseUrl = null,
124+
apiVersion = null,
125+
validateOnStartup = true,
126+
)
124127

125128
val quote = client.stocks().quote("AAPL")
126129
println(quote)

src/main/java/com/marketdata/sdk/MarketDataClient.java

Lines changed: 51 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,19 @@
1919
* (HTTP/2, 2 s connect timeout) for connection pooling (ADR-004) and a 50-permit semaphore that
2020
* gates the global concurrency pool required by SDK requirements §12.
2121
*
22-
* <p>Construction follows the configuration cascade in §4: explicit builder values → {@code
23-
* MARKETDATA_*} environment variables → values in a {@code .env} file in the working directory →
24-
* built-in defaults. Pass no token to enter <em>demo mode</em> (authenticated endpoints will fail;
25-
* the {@code Authorization} header is omitted).
22+
* <p>Two constructors:
23+
*
24+
* <ul>
25+
* <li>{@link #MarketDataClient()} — production path. Resolves everything from the cascade in §4
26+
* ({@code MARKETDATA_*} environment variable → value in a {@code .env} file → built-in
27+
* default). With no token in the cascade, enters <em>demo mode</em> — authenticated endpoints
28+
* will fail and the {@code Authorization} header is omitted.
29+
* <li>{@link #MarketDataClient(String, String, String, boolean)} — explicit-control path for
30+
* tests and short-lived runtimes. Each parameter may still be {@code null} to defer to the
31+
* cascade for that single value.
32+
* </ul>
33+
*
34+
* <p>Instances are immutable: every field is {@code final} and assigned in the constructor.
2635
*/
2736
public final class MarketDataClient implements AutoCloseable {
2837

@@ -48,18 +57,46 @@ public final class MarketDataClient implements AutoCloseable {
4857
private final boolean demoMode;
4958
private final boolean validateOnStartup;
5059

51-
private MarketDataClient(Builder builder) {
60+
/**
61+
* Production constructor. Resolves all settings from the configuration cascade in SDK
62+
* requirements §4 (env var → {@code .env} → built-in default) and enables startup validation.
63+
*
64+
* <p>Equivalent to {@link #MarketDataClient(String, String, String, boolean) new
65+
* MarketDataClient(null, null, null, true)}.
66+
*/
67+
public MarketDataClient() {
68+
this(null, null, null, true);
69+
}
70+
71+
/**
72+
* Explicit-control constructor for tests and short-lived runtimes. Each of {@code apiKey}, {@code
73+
* baseUrl}, and {@code apiVersion} may be {@code null} to defer to the cascade in §4 for that
74+
* single value.
75+
*
76+
* @param apiKey explicit API token, or {@code null} to resolve from {@code MARKETDATA_TOKEN} →
77+
* {@code .env} → demo mode
78+
* @param baseUrl override the API base URL, or {@code null} to resolve to {@link
79+
* Configuration#DEFAULT_BASE_URL}
80+
* @param apiVersion override the API version segment, or {@code null} to resolve to {@link
81+
* Configuration#DEFAULT_API_VERSION}
82+
* @param validateOnStartup whether to validate the token on construction by calling {@code
83+
* /user/} (SDK requirements §5). Pass {@code false} for short-lived runtimes where the
84+
* startup hit is undesirable.
85+
*/
86+
public MarketDataClient(
87+
@Nullable String apiKey,
88+
@Nullable String baseUrl,
89+
@Nullable String apiVersion,
90+
boolean validateOnStartup) {
5291
Configuration config = Configuration.loadFromProcess();
53-
this.token = config.resolve(builder.apiKey, EnvVars.TOKEN);
92+
this.token = config.resolve(apiKey, EnvVars.TOKEN);
5493
this.baseUrl =
5594
trimTrailingSlash(
56-
config.resolveOrDefault(
57-
builder.baseUrl, EnvVars.BASE_URL, Configuration.DEFAULT_BASE_URL));
95+
config.resolveOrDefault(baseUrl, EnvVars.BASE_URL, Configuration.DEFAULT_BASE_URL));
5896
this.apiVersion =
59-
config.resolveOrDefault(
60-
builder.apiVersion, EnvVars.API_VERSION, Configuration.DEFAULT_API_VERSION);
97+
config.resolveOrDefault(apiVersion, EnvVars.API_VERSION, Configuration.DEFAULT_API_VERSION);
6198
this.demoMode = this.token == null;
62-
this.validateOnStartup = builder.validateOnStartup;
99+
this.validateOnStartup = validateOnStartup;
63100
this.userAgent = "marketdata-sdk-java/" + Version.current();
64101

65102
this.httpClient =
@@ -73,23 +110,19 @@ private MarketDataClient(Builder builder) {
73110
LOG.log(
74111
Level.INFO,
75112
"Initialized Market Data SDK {0} (baseUrl={1}, apiVersion={2}, demoMode={3})",
76-
new Object[] {Version.current(), baseUrl, apiVersion, demoMode});
77-
if (demoMode) {
113+
new Object[] {Version.current(), this.baseUrl, this.apiVersion, this.demoMode});
114+
if (this.demoMode) {
78115
LOG.warning(
79116
"No API token provided — running in demo mode. Authenticated endpoints will"
80117
+ " fail; rate-limit initialization is skipped.");
81118
} else if (LOG.isLoggable(Level.FINE)) {
82-
LOG.log(Level.FINE, "Token: {0}", Tokens.redact(token));
119+
LOG.log(Level.FINE, "Token: {0}", Tokens.redact(this.token));
83120
}
84121

85122
// SDK requirements §5: validate on startup by default. The actual
86123
// /user/ call lands with the request layer; this flag is the seam.
87124
}
88125

89-
public static Builder builder() {
90-
return new Builder();
91-
}
92-
93126
public String getBaseUrl() {
94127
return baseUrl;
95128
}
@@ -126,45 +159,4 @@ public void close() {
126159
private static String trimTrailingSlash(String url) {
127160
return url.endsWith("/") ? url.substring(0, url.length() - 1) : url;
128161
}
129-
130-
public static final class Builder {
131-
private @Nullable String apiKey;
132-
private @Nullable String baseUrl;
133-
private @Nullable String apiVersion;
134-
private boolean validateOnStartup = true;
135-
136-
private Builder() {}
137-
138-
/** Override the API token; otherwise resolved from {@code MARKETDATA_TOKEN} or {@code .env}. */
139-
public Builder apiKey(String apiKey) {
140-
this.apiKey = apiKey;
141-
return this;
142-
}
143-
144-
/** Override the base URL (default {@value Configuration#DEFAULT_BASE_URL}). */
145-
public Builder baseUrl(String baseUrl) {
146-
this.baseUrl = baseUrl;
147-
return this;
148-
}
149-
150-
/** Override the API version (default {@value Configuration#DEFAULT_API_VERSION}). */
151-
public Builder apiVersion(String apiVersion) {
152-
this.apiVersion = apiVersion;
153-
return this;
154-
}
155-
156-
/**
157-
* Whether to validate the token at construction by calling {@code /user/} (SDK requirements
158-
* §5). Defaults to {@code true}. Disable for short-lived runtimes where the startup hit is
159-
* undesirable.
160-
*/
161-
public Builder validateOnStartup(boolean validateOnStartup) {
162-
this.validateOnStartup = validateOnStartup;
163-
return this;
164-
}
165-
166-
public MarketDataClient build() {
167-
return new MarketDataClient(this);
168-
}
169-
}
170162
}

src/test/java/com/marketdata/sdk/MarketDataClientTest.java

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class MarketDataClientTest {
99

1010
@Test
1111
void buildsWithExplicitToken() {
12-
try (var client = MarketDataClient.builder().apiKey("test-key").build()) {
12+
try (var client = new MarketDataClient("test-key", null, null, true)) {
1313
assertThat(client.isDemoMode()).isFalse();
1414
assertThat(client.getBaseUrl()).isEqualTo(Configuration.DEFAULT_BASE_URL);
1515
assertThat(client.getApiVersion()).isEqualTo(Configuration.DEFAULT_API_VERSION);
@@ -18,26 +18,41 @@ void buildsWithExplicitToken() {
1818

1919
@Test
2020
void demoModeWhenNoTokenAvailable() {
21-
// No apiKey set on the builder. Demo mode iff the env/dotenv
21+
// No apiKey passed to the constructor. Demo mode iff the env/dotenv
2222
// cascade also yields nothing — true on any CI environment that
2323
// doesn't export MARKETDATA_TOKEN. This assertion is conditional
2424
// so the test stays valid in both cases.
25-
try (var client = MarketDataClient.builder().build()) {
25+
try (var client = new MarketDataClient()) {
2626
String envToken = System.getenv("MARKETDATA_TOKEN");
2727
boolean expectDemo = envToken == null || envToken.isBlank();
2828
assertThat(client.isDemoMode()).isEqualTo(expectDemo);
2929
}
3030
}
3131

32+
@Test
33+
void noArgConstructorAppliesProductionDefaults() {
34+
// The no-arg constructor must be equivalent to `new MarketDataClient(null, null, null,
35+
// true)` — production path with everything resolved from the cascade and startup
36+
// validation enabled. validateOnStartup and the userAgent format are env-independent,
37+
// so we assert them unconditionally; baseUrl/apiVersion fall back to the documented
38+
// defaults only when the cascade has no override, so we gate those assertions on the
39+
// env vars being unset (mirrors the demo-mode test above).
40+
try (var client = new MarketDataClient()) {
41+
assertThat(client.isValidateOnStartup()).isTrue();
42+
assertThat(client.getUserAgent()).startsWith("marketdata-sdk-java/");
43+
44+
if (System.getenv("MARKETDATA_BASE_URL") == null) {
45+
assertThat(client.getBaseUrl()).isEqualTo(Configuration.DEFAULT_BASE_URL);
46+
}
47+
if (System.getenv("MARKETDATA_API_VERSION") == null) {
48+
assertThat(client.getApiVersion()).isEqualTo(Configuration.DEFAULT_API_VERSION);
49+
}
50+
}
51+
}
52+
3253
@Test
3354
void overridesAreHonored() {
34-
try (var client =
35-
MarketDataClient.builder()
36-
.apiKey("KEY")
37-
.baseUrl("https://example.test/")
38-
.apiVersion("v2")
39-
.validateOnStartup(false)
40-
.build()) {
55+
try (var client = new MarketDataClient("KEY", "https://example.test/", "v2", false)) {
4156
assertThat(client.getBaseUrl()).isEqualTo("https://example.test"); // trailing slash trimmed
4257
assertThat(client.getApiVersion()).isEqualTo("v2");
4358
assertThat(client.isValidateOnStartup()).isFalse();
@@ -46,14 +61,14 @@ void overridesAreHonored() {
4661

4762
@Test
4863
void userAgentMatchesSpec() {
49-
try (var client = MarketDataClient.builder().apiKey("KEY").build()) {
64+
try (var client = new MarketDataClient("KEY", null, null, true)) {
5065
assertThat(client.getUserAgent()).startsWith("marketdata-sdk-java/");
5166
}
5267
}
5368

5469
@Test
5570
void rateLimitsStartUnpopulated() {
56-
try (var client = MarketDataClient.builder().apiKey("KEY").build()) {
71+
try (var client = new MarketDataClient("KEY", null, null, true)) {
5772
assertThat(client.getRateLimits()).isNull();
5873
}
5974
}

0 commit comments

Comments
 (0)