Skip to content

Epic: OpenAPI migration and environment removal (0.2.0) #72

Description

@MaxMichel2

Summary

devview-networkmock-core currently configures mocks with a bespoke mocks.json format built around an API groups × environments model. This epic tracks replacing that with OpenAPI 3.x as the configuration format, and removing the environment axis in favor of a simpler mocked/live split plus API version awareness.

This issue is a tracking issue only — no code changes here. Each linked issue is independently implementable and reviewable.

Why

  1. The format is bespoke. Integrators hand-write mocks.json even though most already own an OpenAPI spec describing the same paths, methods, operation ids, and example responses — plus things mocks.json cannot express (response headers, declared status codes, parameter schemas).

  2. Environments are the wrong axis. A running app talks to exactly one base URL at a time (whichever its build points at). Modelling staging and production simultaneously means:

    • the UI renders a tab per group×environment, so roughly half the tabs are dead weight for any given build — toggling a mock in the "staging" tab does nothing when the app is built against "prod" (devview-networkmock/src/commonMain/kotlin/com/worldline/devview/networkmock/NetworkMockScreen.kt:151-165, viewmodel/NetworkMockViewModel.kt:91-104)
    • environmentId is baked into the DataStore key (EndpointKey.compositeKey, devview-networkmock-core/src/commonMain/kotlin/com/worldline/devview/networkmock/core/model/MockConfiguration.kt:374-387), duplicating persisted state
    • environmentId is also a response-file directory tier (repository/MockConfigRepository.kt:448-449), duplicating response files on disk
    • response-file discovery runs groups × environments × endpoints (devview-networkmock/src/commonMain/kotlin/com/worldline/devview/networkmock/viewmodel/NetworkMockViewModel.kt:161-183), so the environment axis multiplies an already expensive eager-load loop

    The shipped sample proves the axis is being misused for something else: sample/network/src/commonMain/composeResources/files/networkmocks/mocks.json:72-77 uses the prod environment's endpointOverrides to rewrite /api/v1/profile/{userId}/api/v2/profile/{userId}. That is an API version difference wearing an environment costume. The real requirements are: mocked vs. live (already covered by the existing global toggle) plus API versioning.

Outcome

mocks.json and the environment concept are deleted in a breaking 0.2.0 release. Configuration becomes one OpenAPI 3.x document per API group (JSON or YAML). Operations carry a derived version tag (parsed from the path) for display and filtering only — the engine always responds to whatever version the app actually calls.

Key decisions (apply to every linked issue)

Decision Choice
Config format OpenAPI 3.x, JSON and YAML. One spec file = one group. info.title → group name, servers[].url → hosts, paths → operations, operationId → operation id, summary → display name. No manifest file — a group spanning multiple API versions is just multiple paths entries in the same spec (e.g. both /api/v1/profile/{id} and /api/v2/profile/{id}).
Response bodies Stay as external files on disk, referenced from the spec via the standard OpenAPI examples.<name>.externalValue field. No vendor extension needed for this.
Versioning Version is a derived, display-only tag, parsed from the operation's path using a configurable regex (default /v(\d+)), exposed as a constructor parameter the same way responseSuffixes is today. Version is not part of the operation identity — /api/v1/x and /api/v2/x already have distinct operationIds in the spec, so they are naturally distinct operations.
Version behaviour Observed only, for 0.2.0. The engine responds to whatever version the app actually calls. A v2 mock can sit unused until the app (or a feature flag) starts calling v2. Forcing/rewriting the app onto a specific version is a deliberately deferred, separate feature (#85 below) because it would mutate real network traffic and needs its own design pass.
Parser depth Staged. The core parser (JSON + YAML, local #/components refs and external file refs, examples) blocks 0.2.0 since it must fully replace mocks.json. Schema-synthesised response bodies, requestBody matching, and allOf/oneOf/discriminator resolution are follow-up issues in 0.2.x — they add depth but nothing in 0.2.0 depends on them.
Migration Big-bang breaking change at 0.2.0. No dual-format period — this is a pre-1.0 library (gradle.properties version 0.1.4), so a single clean break is preferable to maintaining two parsers and two DataStore key shapes indefinitely. A migration guide and a conversion script ship alongside the break.
Naming Adopt OpenAPI vocabulary only for types that model something OpenAPI describes (endpointoperation, groupspec). Types that model DevView's own runtime mocking behaviour, which OpenAPI has no concept of, keep their current names — see the naming table in #75.
Vendor extensions Runtime-only behavior vanilla OpenAPI can't express (delay today, failure-rate/sequences as follow-ups) lives under an x-devview object — OpenAPI's standard x--prefixed Specification Extensions mechanism. See #94.
Loading strategy Discovery returns metadata only (status code + example name) for the main list; response body content loads lazily, only for the operation whose detail screen is actually open. See #98.
Format extensibility MockConfigRepository (#73) is a pure RawBytes -> ApiSpec seam — no OpenAPI-specific type may leak past it. This keeps a second, lower-ceremony input format (#99) cheap to add later, without deciding to build it now.

Resulting model shape

OperationKey(specId, operationId)             // was EndpointKey(groupId, environmentId, endpointId)
compositeKey = "$specId-$operationId"         // DataStore key shape changes -> needs a one-shot prune, see #78

ApiSpec = one OpenAPI document
  id        <- info.title (slugified)
  name      <- info.title
  servers   <- servers[].url, hostnames extracted   // replaces EnvironmentConfig.url
Operation
  operationId <- operationId
  name        <- summary
  path        <- paths key
  method      <- operation key
  version     <- regex over path, nullable, display-only
  responses   <- responses.<code>.content.*.examples.<name>.externalValue

Linked issues

0.2.0 (blocking, in dependency order)

0.2.x follow-ups (OpenAPI depth, not blocking)

Deferred capability (explicitly out of scope for 0.2.0)

Deferred, demand-gated (raised during external design review, tracked deliberately — not scheduled)

Independent of the migration (found during the audit, can land any time)

Suggested order of work

#73#74#75#76#98#77#94 → (#78, #79 in parallel) → (#80, #81 last, since docs follow the API per this repo's documentation-hygiene rule in CLAUDE.md).

#75 (the rename) is sequenced early so that PRs for #76 onward are written directly in the final vocabulary instead of being renamed twice. #98 (lazy loading) lands before #79 (UI rework) so the UI is built against the final loading model once, not reworked twice. #94 (delay vendor extension) depends only on #73 and can move earlier if convenient — it's placed here to keep the "config model settles, then behavior extensions layer on" reading order.

#86#92 have no dependency on the migration and can be picked up by anyone, at any time, independently. #95#97 and #99 are the same — deliberately tracked, explicitly not scheduled; #99 additionally requires a demonstrated need before it's picked up at all (see its issue body).

Revision note (2026-07-30)

This epic's scope was revised after discussing the migration with an external AI (Gemini) as a design-partner sounding board. That review's central recommendation — keep the bespoke JSON format as DevView's permanent runtime core, treat OpenAPI as an optional adapter — was considered and rejected: it re-opens the dual-format question already closed in favor of the big-bang break, and its supporting arguments (OpenAPI can't express runtime behavior; in-app overrides need a lightweight model) don't hold up against this codebase specifically, since OperationMockState already separates runtime mock state from parsed config, and the "can't express runtime behavior" gap applies equally to the old bespoke format. No existing issue was closed, reversed, or reduced in scope as a result.

Two real gaps and one good architectural point did survive the review, and are reflected above: the delay-simulation feature needed an explicit home in the new format (#94), whole-config eager loading needed to not carry into a world where specs can be much larger (#98), and the parser-as-a-pure-seam design (already implicit in #73) is now an explicit, checked requirement specifically because it's what keeps a second input format (#99) cheap to add later without deciding to build it now.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions