]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commit
feat: resolve #314 — Add charging station template Zod validation with schema version...
authorgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sun, 17 May 2026 22:38:50 +0000 (00:38 +0200)
committerGitHub <noreply@github.com>
Sun, 17 May 2026 22:38:50 +0000 (00:38 +0200)
commit8bb806dc4ae42aafe100009dfd34be4809ebc921
tree05ffd7a750381da5a3756711ab878ef12a6a074d
parent79a4089c3cbd329e9356096f8a6608d43b539228
feat: resolve #314 — Add charging station template Zod validation with schema versioning (#1860)

* feat: add charging station template Zod validation with schema versioning

Add Zod v4 runtime validation for charging station templates with
schema versioning support. This replaces scattered imperative checks
with a declarative schema pipeline that validates, migrates, and
transforms templates at parse time.

New files:
- TemplateMigrations.ts: CURRENT_SCHEMA_VERSION, coerceVersion(),
  migration registry with migrateV0ToV1()
- TemplateSchema.ts: Zod schemas with topology union, loose + strict
  variants, MeterValues value coercion (number -> string)
- TemplateValidation.ts: validateTemplate() pipeline,
  transformTemplate(), TemplateValidationError

Integration:
- getTemplateFromFile() now calls validateTemplate() instead of bare
  JSON.parse(...) as ChargingStationTemplate
- Cache key updated to hash:vN format incorporating schema version
- Removed checkTemplate(), checkConnectorsConfiguration(),
  checkEvsesConfiguration(), warnTemplateKeysDeprecation() from
  Helpers.ts (logic absorbed into schema/pipeline)
- Exported getConfiguredMaxNumberOfConnectors and
  getMaxNumberOfConnectors from Helpers.ts for connector setup

Template changes:
- Added "$schemaVersion": 1 to all 15 template JSON files

Tests:
- TemplateMigrations.test.ts: version coercion, migration, error cases
- TemplateSchema.test.ts: required fields, topology, EVSE validation,
  MeterValues coercion, all 15 templates pass
- TemplateValidation.test.ts: pipeline, transforms, error class, round-trip

Closes #314

* fix(template): address PR #1860 review findings

- coerceVersion(null|undefined) now returns 0 so legacy templates
  without $schemaVersion go through v0->v1 migration; deprecated keys
  (supervisionUrl, authorizationFile, payloadSchemaValidation,
  mustAuthorizeAtRemoteStart) are renamed instead of being silently
  swallowed by looseObject [HIGH]
- transformTemplate uses Math.max(numberOfConnectors[]) as the
  worst-case bound for the randomConnectors auto-trigger; new
  Helpers.ts pair getMaxConfiguredNumberOfConnectors (deterministic
  upper bound) and pickConfiguredNumberOfConnectors (random pick)
  centralize array semantics; getConfiguredMaxNumberOfConnectors now
  delegates to pickConfiguredNumberOfConnectors [HIGH]
- BaseTemplateSchema.$schemaVersion is now z.literal(CURRENT_SCHEMA_VERSION)
  (post-migration assertion); deprecated keys removed from the v1
  schema and explicitly rejected by a superRefine block with a clear
  diagnostic [MEDIUM]
- transformTemplate drops the unreachable templateMaxConnectors < 0
  branch [LOW]
- validateTemplate widens to accept unknown and rejects null,
  non-plain-object and array payloads with a clear BaseError instead
  of crashing later in coerceVersion [LOW]
- SignedMeterValueSchema, UnitOfMeasureSchema and WsOptionsSchema
  replace z.looseObject({}) with typed shapes plus .catchall(z.unknown())
  for forward-compatible vendor extensions [LOW]
- TemplateValidationError now surfaces '(migrated from vX -> vY)' in
  its message so post-migration validation failures are visible to
  operators

Tests: update coerceVersion(null|undefined) expectation to 0; add
end-to-end legacy migration, deprecated-key rejection (Schema and
Validation layers), null/string/array payload guards, array-form
numberOfConnectors auto-trigger regression, dead-branch regression,
migratedFrom message presence, and unit tests for the two new
Helpers.ts helpers.

* fix(template): address second-pass PR #1860 review findings

- normalize $schemaVersion to coerced integer in validateTemplate so the
  no-migration path (when version === CURRENT) also satisfies the strict
  z.literal(CURRENT_SCHEMA_VERSION) check; was failing for user-authored
  templates with string "$schemaVersion": "1" [F1]
- harmonize coerceVersion error wording on "non-negative integer" across
  all rejection branches; the previous "positive integer" message was
  contradictory since 0 is a valid input (legacy/pre-versioning sentinel
  triggering v0 migration) [F2]
- introduce SCHEMA_VERSION_STRING_PATTERN (^\\d+$) gate in coerceVersion
  to reject permissive Number() coercions ('1.0', '0x1', '1e0', ' 1 ',
  '', '+1', '01a'); pattern mirrors the canonical non-negative-integer
  string pattern used in TemplateSchema for connector/EVSE keys [F6]
- replace for...in with Object.entries destructuring in Evses topology
  validation, harmonizing with the prior-art pattern in Helpers.ts and
  removing the redundant template.Evses[evseKey] re-lookup [F3]
- tighten getMaxConfiguredNumberOfConnectors cast to readonly number[]
  matching the helper signature [F5/F7]
- document the cache-bound warning emission frequency in
  transformTemplate's JSDoc: warnings fire once per (templateFile,
  schemaVersion) cache miss rather than per station instance, which is
  template-scoped on purpose [F4]

Tests: assert string "$schemaVersion": "1" is accepted and normalized
to numeric 1; battery of 8 permissive numeric strings rejected with
harmonized wording; coerceVersion rejection branches all use the same
"non-negative integer" diagnostic.

* fix(template): broaden wsOptions.headers and harden template pipeline

- WsOptionsSchema.headers accepts string | number | string[] values,
  matching Node's OutgoingHttpHeader runtime contract; field-names
  enforced non-empty per RFC 9110 §5.1.

- templateHash uses SRI-style `${algorithm}:${schemaVersion}:${contentHash}`
  computed over the validated template, restoring whitespace-insensitive
  cache semantics from pre-PR main while keeping schema-version-bump
  invalidation deterministic.

- Migration registry refactored to sequential n→n+1 chain so future
  schema versions append one function instead of rewriting prior
  migrations (no behavior change at v1).

- validateTemplate clones its input via structuredClone at the
  validation boundary, honoring the repository immutability convention.

BREAKING CHANGE: external log monitors keyed on the literal string
"Failed to read charging station template file" must also match
"Invalid charging station template payload (not a JSON object)" for
JSON-shape errors; file I/O errors retain their previous wording via
handleFileException.

* test: extract mockLoggerWarnDebug helper to dedupe migration logger spies

Replaces 8 occurrences of the warn+debug t.mock.method pair across
TemplateMigrations.test.ts and TemplateValidation.test.ts with a typed
helper sibling to createLoggerMocks.

* chore(deps): deduplicate pnpm-lock entries for @rolldown/pluginutils and ws

---------

Co-authored-by: Coding Agent <agent@e-mobility-simulator.dev>
Co-authored-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
Co-authored-by: Jérôme Benoit <jerome.benoit@sap.com>
28 files changed:
pnpm-lock.yaml
src/assets/station-templates/abb-atg.station-template.json
src/assets/station-templates/abb.station-template.json
src/assets/station-templates/chargex.station-template.json
src/assets/station-templates/evlink.station-template.json
src/assets/station-templates/keba-ocpp2-signed.station-template.json
src/assets/station-templates/keba-ocpp2.station-template.json
src/assets/station-templates/keba.station-template.json
src/assets/station-templates/schneider-evses.station-template.json
src/assets/station-templates/schneider-imredd.station-template.json
src/assets/station-templates/schneider.station-template.json
src/assets/station-templates/siemens.station-template.json
src/assets/station-templates/virtual-simple-atg.station-template.json
src/assets/station-templates/virtual-simple-signed.station-template.json
src/assets/station-templates/virtual-simple.station-template.json
src/assets/station-templates/virtual.station-template.json
src/charging-station/ChargingStation.ts
src/charging-station/Helpers.ts
src/charging-station/TemplateMigrations.ts [new file with mode: 0644]
src/charging-station/TemplateSchema.ts [new file with mode: 0644]
src/charging-station/TemplateValidation.ts [new file with mode: 0644]
src/utils/Utils.ts
tests/charging-station/Helpers.test.ts
tests/charging-station/TemplateMigrations.test.ts [new file with mode: 0644]
tests/charging-station/TemplateSchema.test.ts [new file with mode: 0644]
tests/charging-station/TemplateValidation.test.ts [new file with mode: 0644]
tests/charging-station/helpers/TemplateFixtures.ts [new file with mode: 0644]
tests/helpers/TestLifecycleHelpers.ts