Skip to content

feat(api)!: fold the decode options into one struct - #838

Merged
andiwand merged 1 commit into
mainfrom
feat/decode-options
Sep 6, 2026
Merged

feat(api)!: fold the decode options into one struct#838
andiwand merged 1 commit into
mainfrom
feat/decode-options

Conversation

@andiwand

@andiwand andiwand commented Sep 6, 2026

Copy link
Copy Markdown
Member

🤖 Generated with Claude Code

PR 8 of the v7 API plan.
Stacked on #837.

The asymmetry this fixes

DecodePreference answered which type; CsvOptions answered how to read
it
. They sat on different roads — CsvFile::from_file (file.cpp:331)
constructed internal::csv::CsvFile directly and never touched
open_strategy — so a caller who wanted to name a separator could not get
there through open
. That was the real finding behind the overload count in
the plan.

struct DecodeOptions final {
  std::optional<FileType> as_file_type;
  std::vector<FileType> file_type_priority;
  CsvOptions csv;

  static DecodeOptions as(FileType type);
  static DecodeOptions as_csv(const CsvOptions &options);
};

DecodedFile open(const File &, const DecodeOptions & = {}, const Logger & = …);
DecodedFile open(const std::string &path, const DecodeOptions & = {}, const Logger & = …);

Two declarations where there were six; CsvFile::from_file and
::with_options go.

Two things fell out of it, both worth reading

1. A named type now reports the engine's own refusal. Routing csv through
the strategy first made it worse: open_by_probe catches everything and ends
in UnknownFileType, so from_file's precise NoCsvFile would have been lost —
removing it would have been lossy. The fix is in the strategy, not the tests:

if (options.as_file_type.has_value()) {
  // no next candidate to move on to, so the engine's answer is the answer
  return open_file_as(file, *options.as_file_type, options, logger);
}

open(f, DecodeOptions::as(FileType::rich_text_format)) on non-rtf bytes now
throws NoRtfFile; asking for html throws UnsupportedFileType naming it,
instead of a flat UnknownFileType. Five tests pinned the old collapsing and
are updated, including one whose comment documented it as deliberate —
that rationale ("the caller sees the strategy's own answer") applied when a
named type was still routed through the probe, and no longer holds. Detection
without a named type is untouched and still ends in UnknownFileType.

2. An incoherent dialect stays std::invalid_argument. separator == quote
is a caller mistake, not a claim about the bytes; saying "not a csv" would send
them to look at the file. The csv branch rethrows it rather than folding it in.

Why named factories and not designated initializers

I wrote the call sites as {.as_file_type = X} first. The emscripten
toolchain rejects that
: -Wmissing-designated-field-initializers fires on a
partial designated initializer, CMakeLists.txt:38 sets -Wall -Wextra, and
build_test.yml:119 adds -Werror. AppleClang here does not have the warning,
so the local build was silent — this only surfaced in the syntax check against
the real wasm compiler.

DecodeOptions::as(type) and ::as_csv(options) avoid the construct
altogether and read better at the call site.

Bindings

CsvOptions is mirrored for the first time, alongside the renamed
DecodeOptions:

Java DecodeOptions (was DecodePreference) + CsvOptions; Odr.open(path, options)
Python DecodeOptions / CsvOptions with keyword constructors; plus CsvFile, as_csv_file(), is_csv_file(), which were never bound
ObjC ODRDecodeOptions (was ODRDecodePreference) + ODRCsvOptions; decode(path:options:)
wasm unchanged surface; openAs goes through the struct internally

Verified

Full gtest suite 1456 passed / 6 skipped, 72 python tests, JNI junit.
Apple (11 TUs) and wasm (6 TUs) syntax-checked with -Wall -Wextra -Werror.

Migration

open(file, FileType::csv)             →  open(file, DecodeOptions::as(FileType::csv))
CsvFile::from_file(file, {.separator = ';'})
                                      →  open(file, DecodeOptions::as_csv({.separator = ';'})).as_csv_file()
csv.with_options({.separator = ','})  →  open(csv.file(), DecodeOptions::as_csv({.separator = ','})).as_csv_file()

@andiwand
andiwand force-pushed the feat/mirror-text-encoding branch from a480dda to d744f6b Compare September 6, 2026 13:55
Base automatically changed from feat/mirror-text-encoding to main September 6, 2026 13:57
DecodePreference answered "which type" and CsvOptions answered "how to read
it", on different roads: CsvFile::from_file bypassed open_strategy entirely,
so a caller who wanted to name a separator could not get there through open.
DecodeOptions carries both, and with a default argument the six open overloads
become two.

Two things fell out of routing csv through the strategy. A named type is not a
probe with one candidate, so its engine's own refusal now reaches the caller
instead of UnknownFileType - which is what kept from_file's diagnostics when it
went - and an incoherent dialect stays std::invalid_argument rather than
becoming "not a csv", because it is a caller mistake, not a claim about the
bytes.

DecodeOptions::as and ::as_csv are named factories rather than designated
initializers: partial designated initialisation warns under -Wextra on the
emscripten toolchain, and CI builds with -Werror.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hxDa2rev11eLUEJZJ5nmz
@andiwand
andiwand force-pushed the feat/decode-options branch from 037532d to d4e5968 Compare September 6, 2026 14:04
@andiwand
andiwand merged commit 8bed11b into main Sep 6, 2026
25 checks passed
@andiwand
andiwand deleted the feat/decode-options branch September 6, 2026 14:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant