Skip to content

Commit 4a106eb

Browse files
fix news nullable columns
1 parent aa82825 commit 4a106eb

2 files changed

Lines changed: 70 additions & 1 deletion

File tree

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,10 +222,35 @@ public StockPricesResponse prices(StockPricesRequest request) {
222222
return transport.joinSync(pricesAsync(request));
223223
}
224224

225-
/** Async: fetch news articles for a single symbol. */
225+
/**
226+
* Async: fetch news articles for a single symbol.
227+
*
228+
* <p>Unlike the other endpoints, {@code news} does not support a {@code columns} projection on
229+
* the typed path: {@link com.marketdata.sdk.stocks.StockNewsArticle} declares every field
230+
* non-null, so projecting columns away would force the model to lie (return {@code null} where
231+
* the type promises a value). Rather than silently degrade, a configured {@code columns} filter
232+
* is rejected with a clear message. Consumers who genuinely want a projected payload can use the
233+
* CSV facet — {@code client.stocks().asCsv().columns(...).news(...)} — where the output is raw
234+
* text and no typed contract is broken.
235+
*
236+
* <p>The rejection is surfaced as a <em>failed future</em>, not a synchronous throw, so it
237+
* reaches async callers through their {@code exceptionally}/{@code handle} chain like every other
238+
* failure; the sync {@link #news(StockNewsRequest)} wrapper unwraps it to a direct {@link
239+
* IllegalArgumentException} via {@code join()} (ADR-006).
240+
*/
226241
public java.util.concurrent.CompletableFuture<StockNewsResponse> newsAsync(
227242
StockNewsRequest request) {
243+
if (!config.columns().isEmpty()) {
244+
return java.util.concurrent.CompletableFuture.failedFuture(
245+
new IllegalArgumentException(
246+
"columns projection is not supported on the news endpoint; news always returns all"
247+
+ " fields. For a projected payload use the CSV facet:"
248+
+ " client.stocks().asCsv().columns(...).news(...)"));
249+
}
228250
RequestSpec.Builder b = newsSpec(request);
251+
// Only `columns` is unsupported on news; the rest of the universal params (dateFormat/mode/
252+
// limit/offset) are valid. Past the guard `columns` is empty, so applyTo writes those and
253+
// no-ops the columns clause.
229254
config.applyTo(b);
230255
return execute(
231256
b.build(),

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,50 @@ void newsNoDataEnvelopeYieldsEmptyList() {
355355
assertThat(resp.updated()).isNull();
356356
}
357357

358+
@Test
359+
void newsRejectsColumnsProjectionOnTypedPath() {
360+
// StockNewsArticle is non-null by contract, so a typed columns projection can't be honored
361+
// without lying. It must fail fast and clearly, before any request is dispatched (Option B).
362+
CapturingClient client = okWith(NEWS_BODY);
363+
StocksResource stocks = resourceWith(client);
364+
365+
assertThatThrownBy(() -> stocks.columns("headline").news(StockNewsRequest.of("AAPL")))
366+
.isInstanceOf(IllegalArgumentException.class)
367+
.hasMessageContaining("news")
368+
.hasMessageContaining("asCsv");
369+
// Fail-fast: no request reached the wire.
370+
assertThat(client.captured).isEmpty();
371+
}
372+
373+
@Test
374+
void newsColumnsRejectionIsAFailedFutureNotASyncThrow() {
375+
// ADR-006: the async surface signals errors through the future. The guard must NOT throw at the
376+
// call site (which would bypass .exceptionally/.handle) — newsAsync(...) returns normally and
377+
// the returned future completes exceptionally instead.
378+
CapturingClient client = okWith(NEWS_BODY);
379+
StocksResource stocks = resourceWith(client);
380+
381+
var future = stocks.columns("headline").newsAsync(StockNewsRequest.of("AAPL"));
382+
383+
assertThat(future).isCompletedExceptionally();
384+
assertThatThrownBy(future::join)
385+
.isInstanceOf(java.util.concurrent.CompletionException.class)
386+
.hasCauseInstanceOf(IllegalArgumentException.class);
387+
assertThat(client.captured).isEmpty();
388+
}
389+
390+
@Test
391+
void newsColumnsProjectionStillWorksOnCsvFacet() {
392+
// The CSV facet returns raw text — no typed contract to break — so columns stays supported
393+
// there.
394+
CapturingClient client = okWith("a,b\n1,2");
395+
StocksCsvResource csv = resourceWith(client).asCsv();
396+
397+
assertThat(csv.columns("headline").news(StockNewsRequest.of("AAPL")).csv()).contains("a,b");
398+
String url = URLDecoder.decode(client.captured.get(0).uri().toString(), StandardCharsets.UTF_8);
399+
assertThat(url).contains("columns=headline");
400+
}
401+
358402
// ---------- earnings ----------
359403

360404
@Test

0 commit comments

Comments
 (0)