Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- NetworkMock: a status code with a declared `content.<mediaType>.schema` but no `examples`
now synthesizes a placeholder response body instead of being unmockable — primitives, `enum`
(first value), `object`/`array` (recursively, by declared `type` or by the mere presence of
`properties`/`items`), `allOf` (properties merged; conflicting definitions across members
throw a clear error), and `oneOf` (first declared variant; `discriminator` is parsed but
doesn't yet steer variant selection). Deliberately narrow, not full JSON Schema conformance —
see `docs/modules/networkmock-core.md`'s new "Schema-based response synthesis" section.
`MockResponse` gains `isSynthesized: Boolean` (default `false`); the operation picker page
shows a small "Generated" badge on a synthesized response's row.
(`devview-networkmock-core`, `devview-networkmock`, #82, #84)
- NetworkMock: an operation can now be configured as a **sequence** — an ordered list of
responses it advances through one step per matched request, sticking on the last step once
exhausted rather than looping back to the start. Useful for polling flows (order status,
Expand Down
9 changes: 6 additions & 3 deletions devview-networkmock-core/api/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -96,31 +96,34 @@ package com.worldline.devview.networkmock.core.model {
}

@androidx.compose.runtime.Immutable @kotlinx.serialization.Serializable public final class MockResponse {
ctor public MockResponse(int statusCode, String exampleName, String displayName, String content, optional String contentType, optional java.util.Map<java.lang.String,java.lang.String> headers);
ctor public MockResponse(int statusCode, String exampleName, String displayName, String content, optional String contentType, optional java.util.Map<java.lang.String,java.lang.String> headers, optional boolean isSynthesized);
method public int component1();
method public String component2();
method public String component3();
method public String component4();
method public String component5();
method public java.util.Map<java.lang.String,java.lang.String> component6();
method public com.worldline.devview.networkmock.core.model.MockResponse copy(optional int statusCode, optional String exampleName, optional String displayName, optional String content, optional String contentType, optional java.util.Map<java.lang.String,java.lang.String> headers);
method public boolean component7();
method public com.worldline.devview.networkmock.core.model.MockResponse copy(optional int statusCode, optional String exampleName, optional String displayName, optional String content, optional String contentType, optional java.util.Map<java.lang.String,java.lang.String> headers, optional boolean isSynthesized);
method @InaccessibleFromKotlin public String getContent();
method @InaccessibleFromKotlin public String getContentType();
method @InaccessibleFromKotlin public String getDisplayName();
method @InaccessibleFromKotlin public String getExampleName();
method @InaccessibleFromKotlin public java.util.Map<java.lang.String,java.lang.String> getHeaders();
method @InaccessibleFromKotlin public int getStatusCode();
method @InaccessibleFromKotlin public boolean isSynthesized();
property public String content;
property public String contentType;
property public String displayName;
property public String exampleName;
property public java.util.Map<java.lang.String,java.lang.String> headers;
property public boolean isSynthesized;
property public int statusCode;
field public static final com.worldline.devview.networkmock.core.model.MockResponse.Companion Companion;
}

public static final class MockResponse.Companion {
method public com.worldline.devview.networkmock.core.model.MockResponse create(int statusCode, String exampleName, String content, optional String contentType, optional java.util.Map<java.lang.String,java.lang.String> headers, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.String> statusTextProvider);
method public com.worldline.devview.networkmock.core.model.MockResponse create(int statusCode, String exampleName, String content, optional String contentType, optional java.util.Map<java.lang.String,java.lang.String> headers, optional boolean isSynthesized, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.String> statusTextProvider);
}

@kotlinx.serialization.Serializable public final class NetworkMockState {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ import kotlinx.serialization.Serializable
* @property headers Response headers declared on `responses.<code>.headers` in the spec
* (name to literal `example` value). Does not include `Content-Type`, which is carried
* separately by [contentType]. Empty if the spec declares none.
* @property isSynthesized Whether [content] was generated from a declared `schema` (see
* `com.worldline.devview.networkmock.core.openapi.SchemaSynthesizer`) rather than authored by
* the spec as an `examples.<name>.externalValue` file. `false` for every response variant
* sourced the normal way — this only ever flips to `true` for a status code that declared a
* `schema` but no `examples` of its own.
* @see com.worldline.devview.networkmock.core.repository.MockConfigRepository
*/
@Immutable
Expand All @@ -34,7 +39,8 @@ public data class MockResponse(
val displayName: String,
val content: String,
val contentType: String = "application/json",
val headers: Map<String, String> = emptyMap()
val headers: Map<String, String> = emptyMap(),
val isSynthesized: Boolean = false
) {
public companion object {
/**
Expand All @@ -52,6 +58,8 @@ public data class MockResponse(
* @param content The raw response body
* @param contentType The response's declared media type. Defaults to `"application/json"`.
* @param headers Response headers declared on `responses.<code>.headers`. Defaults to none.
* @param isSynthesized Whether [content] was schema-synthesized rather than author-provided.
* Defaults to `false`.
* @param statusTextProvider Optional lambda that maps a status code to its display
* text. Defaults to the built-in [getStatusText] mapping.
* @return A [MockResponse] with a generated [MockResponse.displayName]
Expand All @@ -62,6 +70,7 @@ public data class MockResponse(
content: String,
contentType: String = "application/json",
headers: Map<String, String> = emptyMap(),
isSynthesized: Boolean = false,
statusTextProvider: (Int) -> String = ::getStatusText
): MockResponse = MockResponse(
statusCode = statusCode,
Expand All @@ -73,7 +82,8 @@ public data class MockResponse(
),
content = content,
contentType = contentType,
headers = headers
headers = headers,
isSynthesized = isSynthesized
)

@Suppress("DocumentationOverPrivateFunction")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ import kotlinx.serialization.Serializable
* that kaml does not provide for `kotlinx.serialization.json.JsonElement`-shaped values.
*
* Only fields consumed by [OpenApiParser] are modeled. Everything else in a real spec
* (`deprecated`, `tags`, `security`, request bodies, schemas, …) is silently ignored via
* lenient/non-strict decoding — this parser mocks, it does not validate.
* (`deprecated`, `tags`, `security`, request bodies, …) is silently ignored via lenient/
* non-strict decoding — this parser mocks, it does not validate. [SchemaObject] is the one
* exception: a `content.<mediaType>.schema` is read to *synthesize* a response body when a
* spec declares no `examples` for a status code (see [SchemaSynthesizer]) — still not
* validation, just a fallback so a schema-only response isn't unmockable.
*/
@Serializable
internal data class OpenApiDocument(
Expand Down Expand Up @@ -112,7 +115,10 @@ internal data class HeaderObject(
)

@Serializable
internal data class MediaTypeObject(val examples: Map<String, ExampleObject> = emptyMap())
internal data class MediaTypeObject(
val examples: Map<String, ExampleObject> = emptyMap(),
val schema: SchemaObject? = null
)

/**
* A named response example, or a `$ref` to one under `components.examples`.
Expand All @@ -128,12 +134,65 @@ internal data class ExampleObject(
val externalValue: String? = null
)

/**
* A JSON Schema (OpenAPI's constrained subset of it) declaration, or a `$ref` to one under
* `components.schemas`. Read only to synthesize a placeholder response body when a
* `content.<mediaType>` declares a [schema] but no `examples` — see [SchemaSynthesizer].
*
* Deliberately not a full JSON Schema model: no `required`, `additionalProperties`,
* `minimum`/`maximum`, string patterns, etc. — anything that would matter for *validation*
* rather than *synthesizing one plausible value*.
*
* @property type The schema's declared type (`"string"`, `"integer"`, `"number"`, `"boolean"`,
* `"object"`, or `"array"`). May be absent when [properties] or [items] alone implies it.
* @property enum If non-empty, [SchemaSynthesizer] uses the first declared value verbatim
* instead of a generic placeholder for [type] `"string"`.
* @property properties For `type: object` (or when present at all, regardless of [type]):
* each property's own schema, synthesized recursively.
* @property items For `type: array` (or when present at all, regardless of [type]): the
* schema of a single array element — [SchemaSynthesizer] produces a one-element array.
* @property nullable Read but not acted on: [SchemaSynthesizer] always synthesizes a real
* value, even for a nullable schema — this library mocks, it does not test null-handling.
* @property format Read but not currently used by [SchemaSynthesizer] — reserved for a future
* format-aware placeholder (e.g. `"date-time"`, `"uuid"`).
* @property allOf Member schemas merged into one effective object schema — see
* [SchemaSynthesizer] for the conflicting-property-definition error case.
* @property oneOf Alternative schemas; [SchemaSynthesizer] synthesizes the first declared
* variant regardless of [discriminator] (see [DiscriminatorObject]'s KDoc for why).
* @property discriminator Parsed but not currently used to select a `oneOf` variant — there is
* no concrete request/response data at spec-parse time to disambiguate against.
*/
@Serializable
internal data class SchemaObject(
@SerialName("\$ref") val ref: String? = null,
val type: String? = null,
val enum: List<String>? = null,
val properties: Map<String, SchemaObject>? = null,
val items: SchemaObject? = null,
val nullable: Boolean? = null,
val format: String? = null,
val allOf: List<SchemaObject>? = null,
val oneOf: List<SchemaObject>? = null,
val discriminator: DiscriminatorObject? = null
)

/**
* A `oneOf` discriminator declaration — identifies which property carries the type tag, and
* optionally maps its values to explicit `components.schemas` names. See [SchemaObject.discriminator].
*/
@Serializable
internal data class DiscriminatorObject(
val propertyName: String = "",
val mapping: Map<String, String>? = null
)

@Serializable
internal data class ComponentsObject(
val parameters: Map<String, ParameterObject> = emptyMap(),
val responses: Map<String, ResponseObject> = emptyMap(),
val examples: Map<String, ExampleObject> = emptyMap(),
val headers: Map<String, HeaderObject> = emptyMap()
val headers: Map<String, HeaderObject> = emptyMap(),
val schemas: Map<String, SchemaObject> = emptyMap()
)

/**
Expand Down
Loading
Loading