From b5f696aa97a645d5f1c079ada65355fd71dc42a0 Mon Sep 17 00:00:00 2001 From: Norbert Hartl Date: Sat, 1 Aug 2026 16:13:22 +0200 Subject: [PATCH] Implement OAI 3.0 parameter style/explode serialization style/explode were parsed and stored on OAParameter but never applied when building a request - every parameter location silently ignored them and just called value asString, which also meant: - Path parameters with a non-string value (e.g. a bare Integer id) crashed outright (ZnUrl>>addPathSegments: requires Strings). - Header/cookie array values serialized as Smalltalk's Array printString (e.g. "#(3 4 5)") instead of a comma-joined string. - matrix/label path styles, spaceDelimited/pipeDelimited/deepObject query styles, and explode:false were all silently no-ops. Added a shared value-serialization layer on OAParameterLocation (isArrayValue:/isObjectValue:/explodeFor:/serializePairsFor:name: defaultStyle: and friends) implementing the full style matrix: - path: simple (default), label, matrix - query: form (default), spaceDelimited, pipeDelimited, deepObject - header: simple only; cookie: form only - explode defaults to true only for style:form, false otherwise; deepObject is only ever meaningfully defined with explode:true Most styles produce a single serialized string; form+explode:true objects and deepObject flatten into multiple top-level query parameters instead (one pair per property), which is why serializePairsFor: returns a collection of key->value pairs rather than a single value - the query location adds each pair, while path/ header (which can never produce more than one) just take the first. Cookies are a documented special case: the spec leaves form+ explode:true with a non-primitive value undefined for cookies (you can't repeat a cookie name the way you can repeat a query key), so arrays/objects are comma-joined into the single cookie value instead of expanded into separate pairs. 14 new tests covering the well-defined style/explode combinations, plus the two crash regressions (integer path parameter, array header/ cookie values). 27/27 OpenAPI-Client-Tests, 56/56 OpenAPI-Core-Tests, 4/4 OpenAPI-REST-Tests, 0 regressions. Co-Authored-By: Claude Sonnet 5 --- .../OARequestBuilderTest.class.st | 304 ++++++++++++++---- .../OACookieParameterLocation.class.st | 11 +- .../OAHeaderParametersLocation.class.st | 4 +- .../OpenAPI-Core/OAParameterLocation.class.st | 112 +++++++ .../OAPathParameterLocation.class.st | 6 +- .../OAQueryParameterLocation.class.st | 5 +- 6 files changed, 381 insertions(+), 61 deletions(-) diff --git a/source/OpenAPI-Client-Tests/OARequestBuilderTest.class.st b/source/OpenAPI-Client-Tests/OARequestBuilderTest.class.st index af38b89..105806e 100644 --- a/source/OpenAPI-Client-Tests/OARequestBuilderTest.class.st +++ b/source/OpenAPI-Client-Tests/OARequestBuilderTest.class.st @@ -40,62 +40,19 @@ OARequestBuilderTest >> testAddHeaderParameterSetsRequestHeader [ ] { #category : 'tests' } -OARequestBuilderTest >> testWriteBodyWithAllOfComposedSchemaDoesNotCrash [ - "allOf-composed schemas (no direct type/properties keyword, like petstoreExpanded's - Pet = allOf[NewPet, {id}]) resolve to JSONSchemaAnyObject via #asJSONSchema since - nothing sets schemaClass for a bare allOf. Writing a body against such a schema - must degrade to a plain passthrough rather than crash." - | definition schema mediaType client builder body | - definition := JSONSchemaDefinition new - allOf: { JSONSchemaDefinition new - properties: { 'name' -> (JSONSchemaDefinition new typeString: 'string'; yourself) } asDictionary; - yourself }; - yourself. - schema := definition asJSONSchema. - self assert: schema class equals: JSONSchemaAnyObject. - mediaType := OAMediaTypeObject new schema: schema. - client := ZnClient new. - builder := OARequestBuilder new client: client. - body := Dictionary new at: 'name' put: 'Rex'; yourself. - mediaType writeBody: body builder: builder. - self assert: client request entity contents equals: '{"name":"Rex"}' -] - -{ #category : 'tests' } -OARequestBuilderTest >> testWriteBodyWithNonObjectSchemaDoesNotCrash [ - "Regression test: OAMediaTypeObject>>writeBody:builder: used to send #isAnyObject - unconditionally to the body schema. #isAnyObject is only implemented on - JSONSchemaObject, so any non-object body schema (bare string, array, or an - allOf-composed schema which resolves to JSONSchemaAnyObject) crashed with - #doesNotUnderstand: #isAnyObject when building a real request." - | mediaType client builder | - mediaType := OAMediaTypeObject new schema: JSONSchema string. - client := ZnClient new. - builder := OARequestBuilder new client: client. - mediaType writeBody: 'hello world' builder: builder. - self assert: client request entity contents equals: '"hello world"' -] - -{ #category : 'tests' } -OARequestBuilderTest >> testWriteFormBodyWithAllOfComposedSchemaDoesNotCrash [ - "Same passthrough bug as #writeBody:builder: (both send #isAnyObject - unconditionally), but for the x-www-form-urlencoded path. An allOf-composed - body schema resolves to JSONSchemaAnyObject and must still form-encode." - | definition schema mediaType client builder body | - definition := JSONSchemaDefinition new - allOf: { JSONSchemaDefinition new - properties: { 'name' -> (JSONSchemaDefinition new typeString: 'string'; yourself) } asDictionary; - yourself }; - yourself. - schema := definition asJSONSchema. - self assert: schema class equals: JSONSchemaAnyObject. - mediaType := OAMediaTypeObject new schema: schema. +OARequestBuilderTest >> testCookieParameterArrayValueCommaJoins [ + "Regression: cookie write:value:to: used to send #asString to the raw value - an + Array printed as garbage instead of a comma-joined string. Unlike query parameters, + a single Cookie header cannot repeat the same name for form+explode:true arrays, so + this is deliberately comma-joined rather than left as separate repeated pairs." + | client builder param dict | client := ZnClient new. builder := OARequestBuilder new client: client. - body := Dictionary new at: 'name' put: 'Rex'; yourself. - mediaType writeFormBody: body builder: builder. - self assert: client request entity contentType sub equals: 'x-www-form-urlencoded'. - self assert: (client request entity contents at: 'name') equals: 'Rex' + param := OAParameter new name: 'ids'; in: #cookie; required: true; + schema: (JSONSchemaArray new items: JSONSchema integer); yourself. + dict := Dictionary new at: 'ids' put: #(1 2); yourself. + param copyFrom: dict to: builder. + self assert: (client request headers at: 'Cookie') equals: 'ids=1,2' ] { #category : 'tests' } @@ -169,6 +126,20 @@ OARequestBuilderTest >> testFlattenScalarValues [ self assert: (result at: 'trial_end') equals: 'now' ] +{ #category : 'tests' } +OARequestBuilderTest >> testHeaderParameterArrayValueCommaJoins [ + "Regression: header write:value:to: used to send #asString to the raw value - + an Array printed as garbage (e.g. #(3 4 5)) instead of a comma-joined string." + | client builder param dict | + client := ZnClient new. + builder := OARequestBuilder new client: client. + param := OAParameter new name: 'X-Ids'; in: #header; required: true; + schema: (JSONSchemaArray new items: JSONSchema integer); yourself. + dict := Dictionary new at: 'X-Ids' put: #(3 4 5); yourself. + param copyFrom: dict to: builder. + self assert: (client request headers at: 'X-Ids') equals: '3,4,5' +] + { #category : 'tests' } OARequestBuilderTest >> testHeaderParameterWrittenViaParameterCopyFromTo [ "End-to-end: a header OAParameter must be writable via the same @@ -181,3 +152,228 @@ OARequestBuilderTest >> testHeaderParameterWrittenViaParameterCopyFromTo [ param copyFrom: dict to: builder. self assert: (client request headers at: 'X-Api-Key') equals: 'secret123' ] + +{ #category : 'tests' } +OARequestBuilderTest >> testPathParameterIntegerValueDoesNotCrash [ + "Regression: addPathSegments: requires Strings: a bare Integer path parameter value + used to crash the whole request build with #doesNotUnderstand: #readStream." + | client builder param dict | + client := ZnClient new. + builder := OARequestBuilder new client: client. + param := OAParameter new name: 'id'; in: #path; required: true; schema: JSONSchema integer; yourself. + dict := Dictionary new at: 'id' put: 5; yourself. + param copyFrom: dict to: builder. + builder path: '/pets/{id}'; baseUri: (ZnUrl new scheme: #http; host: 'x'). + self shouldnt: [ builder buildClient ] raise: Error. + self assert: client request url pathPrintString equals: '/pets/5' +] + +{ #category : 'tests' } +OARequestBuilderTest >> testPathParameterLabelStyleArrayExplodeTrue [ + | client builder param dict | + client := ZnClient new. + builder := OARequestBuilder new client: client. + param := OAParameter new name: 'id'; in: #path; required: true; style: 'label'; explode: true; + schema: (JSONSchemaArray new items: JSONSchema string); yourself. + dict := Dictionary new at: 'id' put: #('blue' 'black' 'brown'); yourself. + param copyFrom: dict to: builder. + builder path: '/colors/{id}'; baseUri: (ZnUrl new scheme: #http; host: 'x'). + builder buildClient. + self assert: client request url pathPrintString equals: '/colors/.blue.black.brown' +] + +{ #category : 'tests' } +OARequestBuilderTest >> testPathParameterMatrixStyleArrayExplodeFalse [ + | client builder param dict | + client := ZnClient new. + builder := OARequestBuilder new client: client. + param := OAParameter new name: 'id'; in: #path; required: true; style: 'matrix'; explode: false; + schema: (JSONSchemaArray new items: JSONSchema integer); yourself. + dict := Dictionary new at: 'id' put: #(3 4 5); yourself. + param copyFrom: dict to: builder. + builder path: '/pets/{id}'; baseUri: (ZnUrl new scheme: #http; host: 'x'). + builder buildClient. + self assert: client request url pathPrintString equals: '/pets/;id=3,4,5' +] + +{ #category : 'tests' } +OARequestBuilderTest >> testPathParameterMatrixStyleArrayExplodeTrue [ + | client builder param dict | + client := ZnClient new. + builder := OARequestBuilder new client: client. + param := OAParameter new name: 'id'; in: #path; required: true; style: 'matrix'; explode: true; + schema: (JSONSchemaArray new items: JSONSchema integer); yourself. + dict := Dictionary new at: 'id' put: #(3 4 5); yourself. + param copyFrom: dict to: builder. + builder path: '/pets/{id}'; baseUri: (ZnUrl new scheme: #http; host: 'x'). + builder buildClient. + self assert: client request url pathPrintString equals: '/pets/;id=3;id=4;id=5' +] + +{ #category : 'tests' } +OARequestBuilderTest >> testPathParameterSimpleStyleArrayIsUnaffectedByExplode [ + | client builder param dict | + client := ZnClient new. + builder := OARequestBuilder new client: client. + param := OAParameter new name: 'id'; in: #path; required: true; + schema: (JSONSchemaArray new items: JSONSchema string); yourself. + dict := Dictionary new at: 'id' put: #('blue' 'black' 'brown'); yourself. + param copyFrom: dict to: builder. + builder path: '/colors/{id}'; baseUri: (ZnUrl new scheme: #http; host: 'x'). + builder buildClient. + self assert: client request url pathPrintString equals: '/colors/blue,black,brown' +] + +{ #category : 'tests' } +OARequestBuilderTest >> testQueryParameterDeepObjectStyle [ + | client builder param dict | + client := ZnClient new. + builder := OARequestBuilder new client: client. + param := OAParameter new name: 'id'; in: #query; required: false; style: 'deepObject'; yourself. + dict := Dictionary new at: 'id' put: (Dictionary new at: #role put: 'admin'; yourself); yourself. + param copyFrom: dict to: builder. + builder path: '/pets'; baseUri: (ZnUrl new scheme: #http; host: 'x'). + builder buildClient. + self assert: client request url printString equals: 'http://x:80/pets?id%5Brole%5D=admin' +] + +{ #category : 'tests' } +OARequestBuilderTest >> testQueryParameterFormStyleArrayExplodeFalseCommaJoins [ + | client builder param dict | + client := ZnClient new. + builder := OARequestBuilder new client: client. + param := OAParameter new name: 'tags'; in: #query; required: false; explode: false; + schema: (JSONSchemaArray new items: JSONSchema string); yourself. + dict := Dictionary new at: 'tags' put: #('a' 'b' 'c'); yourself. + param copyFrom: dict to: builder. + builder path: '/pets'; baseUri: (ZnUrl new scheme: #http; host: 'x'). + builder buildClient. + self assert: client request url printString equals: 'http://x:80/pets?tags=a,b,c' +] + +{ #category : 'tests' } +OARequestBuilderTest >> testQueryParameterFormStyleArrayExplodeTrueDefaultRepeatsKey [ + | client builder param dict | + client := ZnClient new. + builder := OARequestBuilder new client: client. + param := OAParameter new name: 'tags'; in: #query; required: false; + schema: (JSONSchemaArray new items: JSONSchema string); yourself. + dict := Dictionary new at: 'tags' put: #('a' 'b' 'c'); yourself. + param copyFrom: dict to: builder. + builder path: '/pets'; baseUri: (ZnUrl new scheme: #http; host: 'x'). + builder buildClient. + self assert: client request url printString equals: 'http://x:80/pets?tags=a&tags=b&tags=c' +] + +{ #category : 'tests' } +OARequestBuilderTest >> testQueryParameterFormStyleObjectExplodeFalseCommaFlattens [ + | client builder param dict | + client := ZnClient new. + builder := OARequestBuilder new client: client. + param := OAParameter new name: 'id'; in: #query; required: false; explode: false; yourself. + dict := Dictionary new at: 'id' put: (Dictionary new at: #role put: 'admin'; yourself); yourself. + param copyFrom: dict to: builder. + builder path: '/pets'; baseUri: (ZnUrl new scheme: #http; host: 'x'). + builder buildClient. + self assert: client request url printString equals: 'http://x:80/pets?id=role,admin' +] + +{ #category : 'tests' } +OARequestBuilderTest >> testQueryParameterFormStyleObjectExplodeTrueDefaultFlattensProperties [ + | client builder param dict | + client := ZnClient new. + builder := OARequestBuilder new client: client. + param := OAParameter new name: 'id'; in: #query; required: false; yourself. + dict := Dictionary new at: 'id' put: (Dictionary new at: #role put: 'admin'; yourself); yourself. + param copyFrom: dict to: builder. + builder path: '/pets'; baseUri: (ZnUrl new scheme: #http; host: 'x'). + builder buildClient. + self assert: client request url printString equals: 'http://x:80/pets?role=admin' +] + +{ #category : 'tests' } +OARequestBuilderTest >> testQueryParameterPipeDelimitedStyle [ + | client builder param dict | + client := ZnClient new. + builder := OARequestBuilder new client: client. + param := OAParameter new name: 'tags'; in: #query; required: false; style: 'pipeDelimited'; + schema: (JSONSchemaArray new items: JSONSchema string); yourself. + dict := Dictionary new at: 'tags' put: #('a' 'b' 'c'); yourself. + param copyFrom: dict to: builder. + builder path: '/pets'; baseUri: (ZnUrl new scheme: #http; host: 'x'). + builder buildClient. + self assert: client request url printString equals: 'http://x:80/pets?tags=a%7Cb%7Cc' +] + +{ #category : 'tests' } +OARequestBuilderTest >> testQueryParameterSpaceDelimitedStyle [ + | client builder param dict | + client := ZnClient new. + builder := OARequestBuilder new client: client. + param := OAParameter new name: 'tags'; in: #query; required: false; style: 'spaceDelimited'; + schema: (JSONSchemaArray new items: JSONSchema string); yourself. + dict := Dictionary new at: 'tags' put: #('a' 'b' 'c'); yourself. + param copyFrom: dict to: builder. + builder path: '/pets'; baseUri: (ZnUrl new scheme: #http; host: 'x'). + builder buildClient. + self assert: client request url printString equals: 'http://x:80/pets?tags=a%20b%20c' +] + +{ #category : 'tests' } +OARequestBuilderTest >> testWriteBodyWithAllOfComposedSchemaDoesNotCrash [ + "allOf-composed schemas (no direct type/properties keyword, like petstoreExpanded's + Pet = allOf[NewPet, {id}]) resolve to JSONSchemaAnyObject via #asJSONSchema since + nothing sets schemaClass for a bare allOf. Writing a body against such a schema + must degrade to a plain passthrough rather than crash." + | definition schema mediaType client builder body | + definition := JSONSchemaDefinition new + allOf: { JSONSchemaDefinition new + properties: { 'name' -> (JSONSchemaDefinition new typeString: 'string'; yourself) } asDictionary; + yourself }; + yourself. + schema := definition asJSONSchema. + self assert: schema class equals: JSONSchemaAnyObject. + mediaType := OAMediaTypeObject new schema: schema. + client := ZnClient new. + builder := OARequestBuilder new client: client. + body := Dictionary new at: 'name' put: 'Rex'; yourself. + mediaType writeBody: body builder: builder. + self assert: client request entity contents equals: '{"name":"Rex"}' +] + +{ #category : 'tests' } +OARequestBuilderTest >> testWriteBodyWithNonObjectSchemaDoesNotCrash [ + "Regression test: OAMediaTypeObject>>writeBody:builder: used to send #isAnyObject + unconditionally to the body schema. #isAnyObject is only implemented on + JSONSchemaObject, so any non-object body schema (bare string, array, or an + allOf-composed schema which resolves to JSONSchemaAnyObject) crashed with + #doesNotUnderstand: #isAnyObject when building a real request." + | mediaType client builder | + mediaType := OAMediaTypeObject new schema: JSONSchema string. + client := ZnClient new. + builder := OARequestBuilder new client: client. + mediaType writeBody: 'hello world' builder: builder. + self assert: client request entity contents equals: '"hello world"' +] + +{ #category : 'tests' } +OARequestBuilderTest >> testWriteFormBodyWithAllOfComposedSchemaDoesNotCrash [ + "Same passthrough bug as #writeBody:builder: (both send #isAnyObject + unconditionally), but for the x-www-form-urlencoded path. An allOf-composed + body schema resolves to JSONSchemaAnyObject and must still form-encode." + | definition schema mediaType client builder body | + definition := JSONSchemaDefinition new + allOf: { JSONSchemaDefinition new + properties: { 'name' -> (JSONSchemaDefinition new typeString: 'string'; yourself) } asDictionary; + yourself }; + yourself. + schema := definition asJSONSchema. + self assert: schema class equals: JSONSchemaAnyObject. + mediaType := OAMediaTypeObject new schema: schema. + client := ZnClient new. + builder := OARequestBuilder new client: client. + body := Dictionary new at: 'name' put: 'Rex'; yourself. + mediaType writeFormBody: body builder: builder. + self assert: client request entity contentType sub equals: 'x-www-form-urlencoded'. + self assert: (client request entity contents at: 'name') equals: 'Rex' +] diff --git a/source/OpenAPI-Core/OACookieParameterLocation.class.st b/source/OpenAPI-Core/OACookieParameterLocation.class.st index ade5088..fdadc7e 100644 --- a/source/OpenAPI-Core/OACookieParameterLocation.class.st +++ b/source/OpenAPI-Core/OACookieParameterLocation.class.st @@ -9,6 +9,13 @@ Class { { #category : 'writing' } OACookieParameterLocation >> write: key value: value to: builder [ "Previously missing entirely - see OAHeaderParametersLocation>>write:value:to: - for the same gap affecting 'in: #cookie' parameters." - builder addCookieParameter: key value: value + for the same gap affecting 'in: #cookie' parameters. Unlike query parameters, a + single Cookie header cannot repeat the same name for form+explode:true arrays - + comma-join instead of passing the raw collection through (the spec leaves this + combination undefined for cookies; this is the pragmatic, documented choice)." + (self serializePairsFor: value name: key defaultStyle: 'form') do: [ :pair | | v | + v := pair value. + builder addCookieParameter: pair key value: (v isString + ifTrue: [ v ] + ifFalse: [ ',' join: v ]) ] ] diff --git a/source/OpenAPI-Core/OAHeaderParametersLocation.class.st b/source/OpenAPI-Core/OAHeaderParametersLocation.class.st index f064f6c..c6abab4 100644 --- a/source/OpenAPI-Core/OAHeaderParametersLocation.class.st +++ b/source/OpenAPI-Core/OAHeaderParametersLocation.class.st @@ -17,5 +17,7 @@ OAHeaderParametersLocation >> write: key value: value to: builder [ copyFrom:to: dispatches here for any 'in: #header' parameter, and crashed with #doesNotUnderstand: #write:value:to: for every client request using a header parameter (e.g. an API key sent via a custom header)." - builder addHeaderParameter: key value: value + | pairs | + pairs := self serializePairsFor: value name: key defaultStyle: 'simple'. + builder addHeaderParameter: key value: pairs first value ] diff --git a/source/OpenAPI-Core/OAParameterLocation.class.st b/source/OpenAPI-Core/OAParameterLocation.class.st index 15b3596..48a1604 100644 --- a/source/OpenAPI-Core/OAParameterLocation.class.st +++ b/source/OpenAPI-Core/OAParameterLocation.class.st @@ -35,3 +35,115 @@ OAParameterLocation >> parameterName [ OAParameterLocation >> parameterSchema [ ^ parameter schema. ] + +{ #category : 'value serialization' } +OAParameterLocation >> arrayPairsFor: value name: key style: aStyleString explode: explode [ + "Returns an OrderedCollection of one key->value Association. The value is a single + String for every style/explode combination except form+explode:true, where it stays + a collection of strings so the caller can add it as a repeated query parameter + (one key=value pair per element) instead of joining it into one value." + | strings | + strings := value collect: [ :each | self stringifyScalar: each ]. + aStyleString = 'matrix' ifTrue: [ + ^ OrderedCollection with: key -> (explode + ifTrue: [ strings inject: '' into: [ :acc :s | acc , ';' , key , '=' , s ] ] + ifFalse: [ ';' , key , '=' , (',' join: strings) ]) ]. + aStyleString = 'label' ifTrue: [ + ^ OrderedCollection with: key -> ('.' , (explode + ifTrue: [ '.' join: strings ] + ifFalse: [ ',' join: strings ])) ]. + aStyleString = 'spaceDelimited' ifTrue: [ + ^ OrderedCollection with: key -> (' ' join: strings) ]. + aStyleString = 'pipeDelimited' ifTrue: [ + ^ OrderedCollection with: key -> ('|' join: strings) ]. + aStyleString = 'simple' ifTrue: [ + ^ OrderedCollection with: key -> (',' join: strings) ]. + "form (query/cookie)" + ^ OrderedCollection with: key -> (explode + ifTrue: [ strings ] + ifFalse: [ ',' join: strings ]) +] + +{ #category : 'value serialization' } +OAParameterLocation >> explodeFor: aStyleString [ + "Per spec, explode defaults to true only for style:form; every other style + (simple/label/matrix/spaceDelimited/pipeDelimited) defaults to false. deepObject is + only ever meaningfully defined with explode:true, so it is treated as always-exploded + regardless of what was actually set." + aStyleString = 'deepObject' ifTrue: [ ^ true ]. + parameter explode ifNotNil: [ ^ parameter explode ]. + ^ aStyleString = 'form' +] + +{ #category : 'value serialization' } +OAParameterLocation >> isArrayValue: value [ + ^ (value isKindOf: SequenceableCollection) and: [ value isString not and: [ value isSymbol not ] ] +] + +{ #category : 'value serialization' } +OAParameterLocation >> isObjectValue: value [ + ^ (value isKindOf: Dictionary) or: [ value isKindOf: OrderedDictionary ] +] + +{ #category : 'value serialization' } +OAParameterLocation >> objectPairsFor: value name: key style: aStyleString explode: explode [ + "Returns an OrderedCollection of key->value String Associations - exactly one for + simple/label/matrix and form+explode:false, but one PER PROPERTY for deepObject and + form+explode:true (the object is flattened into separate top-level parameters)." + | propertyKeys flatPairs | + propertyKeys := value keys asOrderedCollection. + aStyleString = 'deepObject' ifTrue: [ + ^ propertyKeys collect: [ :k | (key , '[' , k asString , ']') -> (self stringifyScalar: (value at: k)) ] ]. + aStyleString = 'form' ifTrue: [ + explode ifTrue: [ + ^ propertyKeys collect: [ :k | k asString -> (self stringifyScalar: (value at: k)) ] ]. + flatPairs := propertyKeys inject: OrderedCollection new into: [ :acc :k | + acc add: k asString; add: (self stringifyScalar: (value at: k)); yourself ]. + ^ OrderedCollection with: key -> (',' join: flatPairs) ]. + "simple/label/matrix" + explode ifTrue: [ + | kvPairs | + kvPairs := propertyKeys collect: [ :k | k asString , '=' , (self stringifyScalar: (value at: k)) ]. + aStyleString = 'matrix' ifTrue: [ + ^ OrderedCollection with: key -> (kvPairs inject: '' into: [ :acc :p | acc , ';' , p ]) ]. + aStyleString = 'label' ifTrue: [ + ^ OrderedCollection with: key -> ('.' , ('.' join: kvPairs)) ]. + ^ OrderedCollection with: key -> (',' join: kvPairs) ]. + flatPairs := propertyKeys inject: OrderedCollection new into: [ :acc :k | + acc add: k asString; add: (self stringifyScalar: (value at: k)); yourself ]. + aStyleString = 'matrix' ifTrue: [ + ^ OrderedCollection with: key -> (';' , key , '=' , (',' join: flatPairs)) ]. + aStyleString = 'label' ifTrue: [ + ^ OrderedCollection with: key -> ('.' , (',' join: flatPairs)) ]. + ^ OrderedCollection with: key -> (',' join: flatPairs) +] + +{ #category : 'value serialization' } +OAParameterLocation >> primitivePairFor: value name: key style: aStyleString [ + | str | + str := self stringifyScalar: value. + aStyleString = 'matrix' ifTrue: [ ^ OrderedCollection with: key -> (';' , key , '=' , str) ]. + aStyleString = 'label' ifTrue: [ ^ OrderedCollection with: key -> ('.' , str) ]. + ^ OrderedCollection with: key -> str +] + +{ #category : 'value serialization' } +OAParameterLocation >> serializePairsFor: value name: key defaultStyle: aDefaultStyleString [ + "Serializes value per OAI 3.0 style/explode rules (falling back to aDefaultStyleString / + the style-appropriate explode default when the parameter did not set its own). Returns + an OrderedCollection of key->value Associations - usually exactly one, except for + deepObject and form+explode:true objects, which flatten into one pair per property." + | styleName explode | + styleName := parameter style ifNil: [ aDefaultStyleString ]. + explode := self explodeFor: styleName. + (self isObjectValue: value) ifTrue: [ + ^ self objectPairsFor: value name: key style: styleName explode: explode ]. + (self isArrayValue: value) ifTrue: [ + ^ self arrayPairsFor: value name: key style: styleName explode: explode ]. + ^ self primitivePairFor: value name: key style: styleName +] + +{ #category : 'value serialization' } +OAParameterLocation >> stringifyScalar: value [ + ^ value asString +] diff --git a/source/OpenAPI-Core/OAPathParameterLocation.class.st b/source/OpenAPI-Core/OAPathParameterLocation.class.st index 70e4fb8..586e68a 100644 --- a/source/OpenAPI-Core/OAPathParameterLocation.class.st +++ b/source/OpenAPI-Core/OAPathParameterLocation.class.st @@ -18,6 +18,8 @@ OAPathParameterLocation >> extractParameter: aCall [ ] { #category : 'writing' } -OAPathParameterLocation >> write: key value: value to: builder [ - builder addPathParameter: key value: value +OAPathParameterLocation >> write: key value: value to: builder [ + | pairs | + pairs := self serializePairsFor: value name: key defaultStyle: 'simple'. + builder addPathParameter: key value: pairs first value ] diff --git a/source/OpenAPI-Core/OAQueryParameterLocation.class.st b/source/OpenAPI-Core/OAQueryParameterLocation.class.st index c10817d..487e26c 100644 --- a/source/OpenAPI-Core/OAQueryParameterLocation.class.st +++ b/source/OpenAPI-Core/OAQueryParameterLocation.class.st @@ -24,6 +24,7 @@ OAQueryParameterLocation >> extractParameter: aCall [ ] { #category : 'as yet unclassified' } -OAQueryParameterLocation >> write: key value: value to: builder [ - builder addQueryParameter: key value: value +OAQueryParameterLocation >> write: key value: value to: builder [ + (self serializePairsFor: value name: key defaultStyle: 'form') do: [ :pair | + builder addQueryParameter: pair key value: pair value ] ]