Skip to content

feat(core): dynamic path A2 — config model, JSON parser and compiler - #59

Merged
thiagoluga merged 4 commits into
masterfrom
feat/dynamic-a2-config-parser
Jun 30, 2026
Merged

feat(core): dynamic path A2 — config model, JSON parser and compiler#59
thiagoluga merged 4 commits into
masterfrom
feat/dynamic-a2-config-parser

Conversation

@thiagoluga

Copy link
Copy Markdown
Owner

Second step of Epic A — dynamic path (D21), fully additive (D25). Builds on A1 (#58): reports can now be expressed as data and compiled into the same runnable report the fluent path produces.

Changes

Abstractions — serializer-agnostic config model (no JSON coupling):

  • ReportConfig, SourceConfig, ColumnConfig, OutputConfig, DestinationConfig
  • IReportConfigParser — parses a document into a ReportConfig
  • IConfigSourceProvider — the dynamic equivalent of a typed source factory: builds an IBatchSource<ReportRecord> from a SourceConfig + the report schema

Core:

  • JsonReportConfigParser (System.Text.Json) — case-insensitive names, string enums, comments/trailing commas, and property-bag values coerced to CLR primitives / ISO DateTime (same convention as JobParameters).
  • ReportConfigCompiler — turns a ReportConfig into a CompiledReport over the positional ReportRecord: columns → Positional(...) getters; source/format/destination resolved from DI by stable id (IConfigSourceProvider.Type, IWriterFactory.Format, IDestinationFactory.Type). All registrations are resolved up front so a missing provider/factory fails fast before the source is built.

The filter is parsed into the config but its compilation is deferred to A4 — the compiler rejects a config that declares one, with a clear message (no silent drop).

Tests (+7, 33 green Core)

  • Full parse with primitive coercion ("limit": 10long, "type": "Integer" → enum, omitted nullabletrue).
  • Empty / whitespace / malformed JSON → ConfigurationException.
  • Golden config compiled and run end-to-end through ReportRunner → rows reach the destination; the provider receives the parsed source section.
  • Filter present → ConfigurationException (deferred to A4).
  • Referenced factory not registered (xlsx) → ConfigurationException.

Acceptance

A2 in PLAN.md: golden config → compiled, runnable report.

Next

A3 (SQL source from config — real IConfigSourceProvider for "sql"), A4 (JsonLogic filter), A5 (DI sugar + dynamic trigger endpoint).

Second step of Epic A (D21). Reports can now be expressed as data and compiled into
the same runnable report the fluent path produces — no parallel pipeline.

- Abstractions: serializer-agnostic config DTOs (ReportConfig, SourceConfig,
  ColumnConfig, OutputConfig, DestinationConfig), the IReportConfigParser contract, and
  IConfigSourceProvider (the dynamic equivalent of a typed source factory). Additive,
  SemVer-minor (D25); the DTOs carry no JSON coupling.
- Core: JsonReportConfigParser (System.Text.Json; case-insensitive, string enums,
  property-bag values converted to CLR primitives / ISO DateTime like JobParameters) and
  ReportConfigCompiler, which builds a CompiledReport over the positional ReportRecord.
  Columns become Positional(...) getters; source/format/destination are resolved from DI
  by stable id (IConfigSourceProvider / IWriterFactory.Format / IDestinationFactory.Type),
  all resolved up front so a missing registration fails fast before the source is built.
- Filter is parsed but compilation is deferred to A4 (the compiler rejects it explicitly).

Tests (+7, 33 green Core): full parse with primitive coercion; empty/malformed rejection;
config compiled and run end-to-end through ReportRunner; filter and missing-factory both
surface a ConfigurationException.
Sonar new-code coverage was 70.6% (< 80%): the converter's Write path and several
Read branches were unexercised. The config parser is read-only by design
(IReportConfigParser exposes only Parse), so Write now throws NotSupportedException
instead of carrying speculative serialization code, and the parse test exercises every
Read value kind (string, long, double, bool, null, ISO DateTime, nested element).
Adds sample 04-dynamic-config-csv demonstrating A2: a report defined entirely in
report.json (no typed POCO) is parsed, compiled and run, writing a real CSV. The JSON
drives the report name, source selection, columns/schema and output/destination
selection; an in-memory IConfigSourceProvider stands in until the SQL config source
(A3), and the CSV/Local factories are pre-wired in DI until config option binding (A5).
Registered in the solution under the samples folder.
@thiagoluga

Copy link
Copy Markdown
Owner Author

Added sample 04-dynamic-config-csv demonstrating this PR end-to-end: a report defined entirely in report.json (no typed POCO) is parsed → compiled → run, writing a real CSV with headers/format/culture all coming from the config. An in-memory IConfigSourceProvider stands in until the SQL config source (A3); the CSV/Local factories are pre-wired in DI until config option binding (A5).

Comment thread samples/04-dynamic-config-csv/Program.cs Dismissed
- Provide DateTimeKind in the sample's seed data (S6562).
- Reword the sample header comment so it is not flagged as commented-out code (S125).
- Use explicit types where the type is not apparent, per the repo .editorconfig
  (csharp_style_var_elsewhere = false) — clears the IDE0008 suggestions on new code.
- Use the concrete JsonReportConfigParser for the test field (CA1859).

No behavior change; 33 green Core tests, sample still runs.
@sonarqubecloud

Copy link
Copy Markdown

@thiagoluga
thiagoluga merged commit 827ff30 into master Jun 30, 2026
5 checks passed
@thiagoluga
thiagoluga deleted the feat/dynamic-a2-config-parser branch June 30, 2026 16:29
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.

2 participants