feat: match operations on request body shape - #133
MaxMichel2 wants to merge 4 commits into
Conversation
Two operations that collide on path, method, and query (e.g. two specs sharing a host, each declaring POST /api/payments) can now be disambiguated by their requestBody shape. A new RequestBodyMatch (requiredFields, discriminatorField, discriminatorValue) is built from an operation requestBody content schema (application/json preferred, otherwise the first declared media type): required fields from the schema required array, and a discriminator literal value from that property own single-value enum. This is narrow matching, not full JSON Schema validation - an operation whose schema yields neither is unaffected (requestBodyMatch stays null, matching any body, same as declaring no requestBody at all). OpenApiDocument gains RequestBodyObject and SchemaObject.required; OperationObject gains requestBody; ComponentsObject gains requestBodies for ref resolution via the existing resolveRef machinery. RequestMatcher gains matchesRequestBody; MockConfigRepository.findMatchingMock gains an optional requestBody parameter threaded into its operation-matching check. devview-networkmock-ktor plugin reads the request body only when it is already a fully in-memory OutgoingContent.ByteArrayContent, the shape Ktor content negotiation produces for a JSON-serialized body - bytes() is a pure, repeatable read, so this never consumes or mutates anything execute() still needs to send. Any other content shape (streaming, multipart, none) is treated as no body rather than risking corruption of live traffic. api.txt regenerated for devview-networkmock-core; docs/modules/networkmock-core.md gains a Request body matching section.
Answers a question about whether the schema synthesis and requestBody matching work raised in PR review: a real-world YAML spec (schemas at the end of the file, three response codes ref-ing the same schema, a requestBody with its own ref and a requestBody.required boolean, folded summary strings, tags block sequence) now has explicit regression coverage. No existing test previously exercised the full YAML pipeline end to end (isYaml sniffing, kaml decode, ref resolution, schema synthesis) - only JSON string fixtures were used throughout MockConfigRepositoryTest, plus an unrelated kaml library smoke test. Confirms strictMode false lenient decoding, order-independent components.schemas lookup, and SchemaSynthesizer resolving a bare top-level dollar-ref before checking type/properties all work together as intended - no code changes needed, this closes a coverage gap only.
|
Re: does schema synthesis handle real-world specs with dollar-ref'd schemas defined at the end of the file? Yes - confirmed and now covered by a regression test (7236295). The full pipeline already handles this by construction:
Gap found while checking this: no existing test exercised the full YAML pipeline end-to-end (extension sniffing -> kaml decode -> ref resolution -> synthesis) - only JSON string fixtures were used throughout MockConfigRepositoryTest, plus an unrelated kaml library smoke test. Added \parses a real-world YAML spec with dollar-ref schemas declared after paths\ mirroring the exact shape from the question (requestBody with its own dollar-ref + required boolean, three response codes dollar-ref-ing the same response schema, folded summary, tags block sequence) - passed first try, no code changes needed. |
Kotlin 2.2+'s multi-dollar string literals (dollar-dollar-quote-quote-quote) let a JSON/YAML fixture write a literal dollar-ref directly instead of the escaped dollar-quote-dollar-quote form every dollar-ref-containing fixture in this file needed before. Purely a readability cleanup of existing test fixtures across the dollar-ref chain and requestBody-matching tests plus the new YAML regression test - no behavior change, no new coverage. Verified: detektFull, cleanTestAndroidHostTest, testAndroidHostTest all green (44/44 in MockConfigRepositoryTest).
…n/Native) Kotlin/Native's frontend rejects certain punctuation in identifiers derived from backtick-quoted function names, even though the JVM/Android backends accept them fine. RequestMatcherTest's `matchesRequestBody ignores the required field's actual value, only checks presence` test tripped this on iosSimulatorArm64/iosArm64/iosX64 (compileTestKotlinIos*): 'Name contains illegal characters: ,.'. Renamed to replace the comma with 'and' - no other backtick test name in the repo contains a comma (checked repo-wide). Verified locally: :devview-networkmock-core:compileTestKotlinIosSimulatorArm64 now succeeds.
Summary
Stacked on #132 (PR chain: #131 → #132 → this) — merge in order.
Two operations that would otherwise collide on path, method, and query (e.g. two specs sharing a host, each declaring
POST /api/payments) can now be disambiguated by theirrequestBodyshape — required fields and/or a discriminator field's value, read narrowly from the operation's declared schema (not full JSON Schema validation).What changed
RequestBodyMatch(new public class indevview-networkmock-core):requiredFields: List<String>,discriminatorField: String?,discriminatorValue: String?.Operation.requestBodyMatch: RequestBodyMatch?—nullwhen the operation declares norequestBody, or its schema yields nothing to check (no required fields, no usable discriminator).OpenApiDocument: addedRequestBodyObject($ref+content),SchemaObject.required,OperationObject.requestBody,ComponentsObject.requestBodies.requestBody's own$refand its schema's$refboth resolve through the existingresolveRefchain-following machinery from fix: resolve dollar-ref chains and disambiguate component sections #131.OpenApiParser'sParseContextgainsresolveRequestBodyandbuildRequestBodyMatch— the latter prefersapplication/jsonamong declared media types, falling back to whichever is declared first; the discriminator's expected value comes from that property's own single-valueenum(documented simplification, consistent with feat: synthesise response bodies from OpenAPI schemas #82/feat: allOf / oneOf / discriminator resolution #84'soneOf+discriminatorhandling).RequestMatcher.matchesRequestBody(configMatch, requestBody)—nullconfig always matches; a non-null config requires every required field present and, if a discriminator field is declared, that field present (and equal to the declared value, if one was resolvable).MockConfigRepository.findMatchingMockgains an optionalrequestBody: String?parameter, checked alongside the existing path/method/query checks inspec.operations.firstOrNull { ... }.devview-networkmock-ktor'sNetworkMockPluginreads the request body via a newextractRequestBodyText— only when it's already anOutgoingContent.ByteArrayContent(the shape Ktor's content negotiation produces for a JSON-serialized body).bytes()on that type is a pure, repeatable read of bytes already fully materialized in memory — not a stream — so this never consumes or mutates anything the laterexecute(requestBuilder)call still needs to send. Any other content shape (streaming, multipart, no content) returnsnull, i.e. is treated as "no body" rather than risking corruption of live traffic.Scope decisions
enumat parse time — there's no concrete request data available at spec-parse time to do anything smarter, mirroring the same simplification already made foroneOf+discriminatorresponse synthesis in feat: synthesise response bodies from OpenAPI schemas #82/feat: allOf / oneOf / discriminator resolution #84.requestBody(application/jsonpreferred), mirroring how query-parameter matching already reads a single literalexamplevalue rather than modeling every media type.Public API
devview-networkmock-core/api/api.txt— purely additive: newRequestBodyMatchclass,Operation.requestBodyMatch(+component9/copyparam),RequestMatcher.matchesRequestBody,MockConfigRepository.findMatchingMock's new optionalrequestBodyparam.docs/modules/networkmock-core.mdupdated with a new "Request body matching" section.Tests
RequestMatcherTest.kt: 12 new tests formatchesRequestBody— null config, null/invalid/non-object body, required-field presence/absence, discriminator match/mismatch/absence, discriminator-without-value (presence-only), and both constraints combined.MockConfigRepositoryTest.kt: 5 new tests — parsingrequiredintorequestBodyMatch, parsing a discriminator's single-valueenum,requestBodyMatchstayingnullwhen there's nothing to check or norequestBodyat all,findMatchingMockdisambiguating by discriminator value, and resolving a$ref'drequestBodyschema viacomponents.schemas.NetworkMockPluginTest.kt: 2 new tests — two specs sharing a host, disambiguated only by body shape, both resolving to the correct mock; and a request whose body matches no declared shape falling through to the network with its original body reaching theMockEnginebyte-for-byte, unconsumed.Verification
All green — detekt clean, full test suite passing (including the two new plugin tests proving both disambiguation and body-integrity-on-fallback), konsist clean, device tests compile, sample app builds.
devview-networkmock-ktor/api/api.txthas no diff (no new public API there).🤖 Generated with GitHub Copilot in JetBrains AI, based on a handoff plan authored by Claude Code.