Skip to content

Commit 9b0ad17

Browse files
authored
Merge pull request #40 from zweidenker/implement-parameter-style-explode-serialization
Implement OAI 3.0 parameter style/explode serialization
2 parents 976f85f + b5f696a commit 9b0ad17

6 files changed

Lines changed: 381 additions & 61 deletions

source/OpenAPI-Client-Tests/OARequestBuilderTest.class.st

Lines changed: 250 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -40,62 +40,19 @@ OARequestBuilderTest >> testAddHeaderParameterSetsRequestHeader [
4040
]
4141

4242
{ #category : 'tests' }
43-
OARequestBuilderTest >> testWriteBodyWithAllOfComposedSchemaDoesNotCrash [
44-
"allOf-composed schemas (no direct type/properties keyword, like petstoreExpanded's
45-
Pet = allOf[NewPet, {id}]) resolve to JSONSchemaAnyObject via #asJSONSchema since
46-
nothing sets schemaClass for a bare allOf. Writing a body against such a schema
47-
must degrade to a plain passthrough rather than crash."
48-
| definition schema mediaType client builder body |
49-
definition := JSONSchemaDefinition new
50-
allOf: { JSONSchemaDefinition new
51-
properties: { 'name' -> (JSONSchemaDefinition new typeString: 'string'; yourself) } asDictionary;
52-
yourself };
53-
yourself.
54-
schema := definition asJSONSchema.
55-
self assert: schema class equals: JSONSchemaAnyObject.
56-
mediaType := OAMediaTypeObject new schema: schema.
57-
client := ZnClient new.
58-
builder := OARequestBuilder new client: client.
59-
body := Dictionary new at: 'name' put: 'Rex'; yourself.
60-
mediaType writeBody: body builder: builder.
61-
self assert: client request entity contents equals: '{"name":"Rex"}'
62-
]
63-
64-
{ #category : 'tests' }
65-
OARequestBuilderTest >> testWriteBodyWithNonObjectSchemaDoesNotCrash [
66-
"Regression test: OAMediaTypeObject>>writeBody:builder: used to send #isAnyObject
67-
unconditionally to the body schema. #isAnyObject is only implemented on
68-
JSONSchemaObject, so any non-object body schema (bare string, array, or an
69-
allOf-composed schema which resolves to JSONSchemaAnyObject) crashed with
70-
#doesNotUnderstand: #isAnyObject when building a real request."
71-
| mediaType client builder |
72-
mediaType := OAMediaTypeObject new schema: JSONSchema string.
73-
client := ZnClient new.
74-
builder := OARequestBuilder new client: client.
75-
mediaType writeBody: 'hello world' builder: builder.
76-
self assert: client request entity contents equals: '"hello world"'
77-
]
78-
79-
{ #category : 'tests' }
80-
OARequestBuilderTest >> testWriteFormBodyWithAllOfComposedSchemaDoesNotCrash [
81-
"Same passthrough bug as #writeBody:builder: (both send #isAnyObject
82-
unconditionally), but for the x-www-form-urlencoded path. An allOf-composed
83-
body schema resolves to JSONSchemaAnyObject and must still form-encode."
84-
| definition schema mediaType client builder body |
85-
definition := JSONSchemaDefinition new
86-
allOf: { JSONSchemaDefinition new
87-
properties: { 'name' -> (JSONSchemaDefinition new typeString: 'string'; yourself) } asDictionary;
88-
yourself };
89-
yourself.
90-
schema := definition asJSONSchema.
91-
self assert: schema class equals: JSONSchemaAnyObject.
92-
mediaType := OAMediaTypeObject new schema: schema.
43+
OARequestBuilderTest >> testCookieParameterArrayValueCommaJoins [
44+
"Regression: cookie write:value:to: used to send #asString to the raw value - an
45+
Array printed as garbage instead of a comma-joined string. Unlike query parameters,
46+
a single Cookie header cannot repeat the same name for form+explode:true arrays, so
47+
this is deliberately comma-joined rather than left as separate repeated pairs."
48+
| client builder param dict |
9349
client := ZnClient new.
9450
builder := OARequestBuilder new client: client.
95-
body := Dictionary new at: 'name' put: 'Rex'; yourself.
96-
mediaType writeFormBody: body builder: builder.
97-
self assert: client request entity contentType sub equals: 'x-www-form-urlencoded'.
98-
self assert: (client request entity contents at: 'name') equals: 'Rex'
51+
param := OAParameter new name: 'ids'; in: #cookie; required: true;
52+
schema: (JSONSchemaArray new items: JSONSchema integer); yourself.
53+
dict := Dictionary new at: 'ids' put: #(1 2); yourself.
54+
param copyFrom: dict to: builder.
55+
self assert: (client request headers at: 'Cookie') equals: 'ids=1,2'
9956
]
10057

10158
{ #category : 'tests' }
@@ -169,6 +126,20 @@ OARequestBuilderTest >> testFlattenScalarValues [
169126
self assert: (result at: 'trial_end') equals: 'now'
170127
]
171128

129+
{ #category : 'tests' }
130+
OARequestBuilderTest >> testHeaderParameterArrayValueCommaJoins [
131+
"Regression: header write:value:to: used to send #asString to the raw value -
132+
an Array printed as garbage (e.g. #(3 4 5)) instead of a comma-joined string."
133+
| client builder param dict |
134+
client := ZnClient new.
135+
builder := OARequestBuilder new client: client.
136+
param := OAParameter new name: 'X-Ids'; in: #header; required: true;
137+
schema: (JSONSchemaArray new items: JSONSchema integer); yourself.
138+
dict := Dictionary new at: 'X-Ids' put: #(3 4 5); yourself.
139+
param copyFrom: dict to: builder.
140+
self assert: (client request headers at: 'X-Ids') equals: '3,4,5'
141+
]
142+
172143
{ #category : 'tests' }
173144
OARequestBuilderTest >> testHeaderParameterWrittenViaParameterCopyFromTo [
174145
"End-to-end: a header OAParameter must be writable via the same
@@ -181,3 +152,228 @@ OARequestBuilderTest >> testHeaderParameterWrittenViaParameterCopyFromTo [
181152
param copyFrom: dict to: builder.
182153
self assert: (client request headers at: 'X-Api-Key') equals: 'secret123'
183154
]
155+
156+
{ #category : 'tests' }
157+
OARequestBuilderTest >> testPathParameterIntegerValueDoesNotCrash [
158+
"Regression: addPathSegments: requires Strings: a bare Integer path parameter value
159+
used to crash the whole request build with #doesNotUnderstand: #readStream."
160+
| client builder param dict |
161+
client := ZnClient new.
162+
builder := OARequestBuilder new client: client.
163+
param := OAParameter new name: 'id'; in: #path; required: true; schema: JSONSchema integer; yourself.
164+
dict := Dictionary new at: 'id' put: 5; yourself.
165+
param copyFrom: dict to: builder.
166+
builder path: '/pets/{id}'; baseUri: (ZnUrl new scheme: #http; host: 'x').
167+
self shouldnt: [ builder buildClient ] raise: Error.
168+
self assert: client request url pathPrintString equals: '/pets/5'
169+
]
170+
171+
{ #category : 'tests' }
172+
OARequestBuilderTest >> testPathParameterLabelStyleArrayExplodeTrue [
173+
| client builder param dict |
174+
client := ZnClient new.
175+
builder := OARequestBuilder new client: client.
176+
param := OAParameter new name: 'id'; in: #path; required: true; style: 'label'; explode: true;
177+
schema: (JSONSchemaArray new items: JSONSchema string); yourself.
178+
dict := Dictionary new at: 'id' put: #('blue' 'black' 'brown'); yourself.
179+
param copyFrom: dict to: builder.
180+
builder path: '/colors/{id}'; baseUri: (ZnUrl new scheme: #http; host: 'x').
181+
builder buildClient.
182+
self assert: client request url pathPrintString equals: '/colors/.blue.black.brown'
183+
]
184+
185+
{ #category : 'tests' }
186+
OARequestBuilderTest >> testPathParameterMatrixStyleArrayExplodeFalse [
187+
| client builder param dict |
188+
client := ZnClient new.
189+
builder := OARequestBuilder new client: client.
190+
param := OAParameter new name: 'id'; in: #path; required: true; style: 'matrix'; explode: false;
191+
schema: (JSONSchemaArray new items: JSONSchema integer); yourself.
192+
dict := Dictionary new at: 'id' put: #(3 4 5); yourself.
193+
param copyFrom: dict to: builder.
194+
builder path: '/pets/{id}'; baseUri: (ZnUrl new scheme: #http; host: 'x').
195+
builder buildClient.
196+
self assert: client request url pathPrintString equals: '/pets/;id=3,4,5'
197+
]
198+
199+
{ #category : 'tests' }
200+
OARequestBuilderTest >> testPathParameterMatrixStyleArrayExplodeTrue [
201+
| client builder param dict |
202+
client := ZnClient new.
203+
builder := OARequestBuilder new client: client.
204+
param := OAParameter new name: 'id'; in: #path; required: true; style: 'matrix'; explode: true;
205+
schema: (JSONSchemaArray new items: JSONSchema integer); yourself.
206+
dict := Dictionary new at: 'id' put: #(3 4 5); yourself.
207+
param copyFrom: dict to: builder.
208+
builder path: '/pets/{id}'; baseUri: (ZnUrl new scheme: #http; host: 'x').
209+
builder buildClient.
210+
self assert: client request url pathPrintString equals: '/pets/;id=3;id=4;id=5'
211+
]
212+
213+
{ #category : 'tests' }
214+
OARequestBuilderTest >> testPathParameterSimpleStyleArrayIsUnaffectedByExplode [
215+
| client builder param dict |
216+
client := ZnClient new.
217+
builder := OARequestBuilder new client: client.
218+
param := OAParameter new name: 'id'; in: #path; required: true;
219+
schema: (JSONSchemaArray new items: JSONSchema string); yourself.
220+
dict := Dictionary new at: 'id' put: #('blue' 'black' 'brown'); yourself.
221+
param copyFrom: dict to: builder.
222+
builder path: '/colors/{id}'; baseUri: (ZnUrl new scheme: #http; host: 'x').
223+
builder buildClient.
224+
self assert: client request url pathPrintString equals: '/colors/blue,black,brown'
225+
]
226+
227+
{ #category : 'tests' }
228+
OARequestBuilderTest >> testQueryParameterDeepObjectStyle [
229+
| client builder param dict |
230+
client := ZnClient new.
231+
builder := OARequestBuilder new client: client.
232+
param := OAParameter new name: 'id'; in: #query; required: false; style: 'deepObject'; yourself.
233+
dict := Dictionary new at: 'id' put: (Dictionary new at: #role put: 'admin'; yourself); yourself.
234+
param copyFrom: dict to: builder.
235+
builder path: '/pets'; baseUri: (ZnUrl new scheme: #http; host: 'x').
236+
builder buildClient.
237+
self assert: client request url printString equals: 'http://x:80/pets?id%5Brole%5D=admin'
238+
]
239+
240+
{ #category : 'tests' }
241+
OARequestBuilderTest >> testQueryParameterFormStyleArrayExplodeFalseCommaJoins [
242+
| client builder param dict |
243+
client := ZnClient new.
244+
builder := OARequestBuilder new client: client.
245+
param := OAParameter new name: 'tags'; in: #query; required: false; explode: false;
246+
schema: (JSONSchemaArray new items: JSONSchema string); yourself.
247+
dict := Dictionary new at: 'tags' put: #('a' 'b' 'c'); yourself.
248+
param copyFrom: dict to: builder.
249+
builder path: '/pets'; baseUri: (ZnUrl new scheme: #http; host: 'x').
250+
builder buildClient.
251+
self assert: client request url printString equals: 'http://x:80/pets?tags=a,b,c'
252+
]
253+
254+
{ #category : 'tests' }
255+
OARequestBuilderTest >> testQueryParameterFormStyleArrayExplodeTrueDefaultRepeatsKey [
256+
| client builder param dict |
257+
client := ZnClient new.
258+
builder := OARequestBuilder new client: client.
259+
param := OAParameter new name: 'tags'; in: #query; required: false;
260+
schema: (JSONSchemaArray new items: JSONSchema string); yourself.
261+
dict := Dictionary new at: 'tags' put: #('a' 'b' 'c'); yourself.
262+
param copyFrom: dict to: builder.
263+
builder path: '/pets'; baseUri: (ZnUrl new scheme: #http; host: 'x').
264+
builder buildClient.
265+
self assert: client request url printString equals: 'http://x:80/pets?tags=a&tags=b&tags=c'
266+
]
267+
268+
{ #category : 'tests' }
269+
OARequestBuilderTest >> testQueryParameterFormStyleObjectExplodeFalseCommaFlattens [
270+
| client builder param dict |
271+
client := ZnClient new.
272+
builder := OARequestBuilder new client: client.
273+
param := OAParameter new name: 'id'; in: #query; required: false; explode: false; yourself.
274+
dict := Dictionary new at: 'id' put: (Dictionary new at: #role put: 'admin'; yourself); yourself.
275+
param copyFrom: dict to: builder.
276+
builder path: '/pets'; baseUri: (ZnUrl new scheme: #http; host: 'x').
277+
builder buildClient.
278+
self assert: client request url printString equals: 'http://x:80/pets?id=role,admin'
279+
]
280+
281+
{ #category : 'tests' }
282+
OARequestBuilderTest >> testQueryParameterFormStyleObjectExplodeTrueDefaultFlattensProperties [
283+
| client builder param dict |
284+
client := ZnClient new.
285+
builder := OARequestBuilder new client: client.
286+
param := OAParameter new name: 'id'; in: #query; required: false; yourself.
287+
dict := Dictionary new at: 'id' put: (Dictionary new at: #role put: 'admin'; yourself); yourself.
288+
param copyFrom: dict to: builder.
289+
builder path: '/pets'; baseUri: (ZnUrl new scheme: #http; host: 'x').
290+
builder buildClient.
291+
self assert: client request url printString equals: 'http://x:80/pets?role=admin'
292+
]
293+
294+
{ #category : 'tests' }
295+
OARequestBuilderTest >> testQueryParameterPipeDelimitedStyle [
296+
| client builder param dict |
297+
client := ZnClient new.
298+
builder := OARequestBuilder new client: client.
299+
param := OAParameter new name: 'tags'; in: #query; required: false; style: 'pipeDelimited';
300+
schema: (JSONSchemaArray new items: JSONSchema string); yourself.
301+
dict := Dictionary new at: 'tags' put: #('a' 'b' 'c'); yourself.
302+
param copyFrom: dict to: builder.
303+
builder path: '/pets'; baseUri: (ZnUrl new scheme: #http; host: 'x').
304+
builder buildClient.
305+
self assert: client request url printString equals: 'http://x:80/pets?tags=a%7Cb%7Cc'
306+
]
307+
308+
{ #category : 'tests' }
309+
OARequestBuilderTest >> testQueryParameterSpaceDelimitedStyle [
310+
| client builder param dict |
311+
client := ZnClient new.
312+
builder := OARequestBuilder new client: client.
313+
param := OAParameter new name: 'tags'; in: #query; required: false; style: 'spaceDelimited';
314+
schema: (JSONSchemaArray new items: JSONSchema string); yourself.
315+
dict := Dictionary new at: 'tags' put: #('a' 'b' 'c'); yourself.
316+
param copyFrom: dict to: builder.
317+
builder path: '/pets'; baseUri: (ZnUrl new scheme: #http; host: 'x').
318+
builder buildClient.
319+
self assert: client request url printString equals: 'http://x:80/pets?tags=a%20b%20c'
320+
]
321+
322+
{ #category : 'tests' }
323+
OARequestBuilderTest >> testWriteBodyWithAllOfComposedSchemaDoesNotCrash [
324+
"allOf-composed schemas (no direct type/properties keyword, like petstoreExpanded's
325+
Pet = allOf[NewPet, {id}]) resolve to JSONSchemaAnyObject via #asJSONSchema since
326+
nothing sets schemaClass for a bare allOf. Writing a body against such a schema
327+
must degrade to a plain passthrough rather than crash."
328+
| definition schema mediaType client builder body |
329+
definition := JSONSchemaDefinition new
330+
allOf: { JSONSchemaDefinition new
331+
properties: { 'name' -> (JSONSchemaDefinition new typeString: 'string'; yourself) } asDictionary;
332+
yourself };
333+
yourself.
334+
schema := definition asJSONSchema.
335+
self assert: schema class equals: JSONSchemaAnyObject.
336+
mediaType := OAMediaTypeObject new schema: schema.
337+
client := ZnClient new.
338+
builder := OARequestBuilder new client: client.
339+
body := Dictionary new at: 'name' put: 'Rex'; yourself.
340+
mediaType writeBody: body builder: builder.
341+
self assert: client request entity contents equals: '{"name":"Rex"}'
342+
]
343+
344+
{ #category : 'tests' }
345+
OARequestBuilderTest >> testWriteBodyWithNonObjectSchemaDoesNotCrash [
346+
"Regression test: OAMediaTypeObject>>writeBody:builder: used to send #isAnyObject
347+
unconditionally to the body schema. #isAnyObject is only implemented on
348+
JSONSchemaObject, so any non-object body schema (bare string, array, or an
349+
allOf-composed schema which resolves to JSONSchemaAnyObject) crashed with
350+
#doesNotUnderstand: #isAnyObject when building a real request."
351+
| mediaType client builder |
352+
mediaType := OAMediaTypeObject new schema: JSONSchema string.
353+
client := ZnClient new.
354+
builder := OARequestBuilder new client: client.
355+
mediaType writeBody: 'hello world' builder: builder.
356+
self assert: client request entity contents equals: '"hello world"'
357+
]
358+
359+
{ #category : 'tests' }
360+
OARequestBuilderTest >> testWriteFormBodyWithAllOfComposedSchemaDoesNotCrash [
361+
"Same passthrough bug as #writeBody:builder: (both send #isAnyObject
362+
unconditionally), but for the x-www-form-urlencoded path. An allOf-composed
363+
body schema resolves to JSONSchemaAnyObject and must still form-encode."
364+
| definition schema mediaType client builder body |
365+
definition := JSONSchemaDefinition new
366+
allOf: { JSONSchemaDefinition new
367+
properties: { 'name' -> (JSONSchemaDefinition new typeString: 'string'; yourself) } asDictionary;
368+
yourself };
369+
yourself.
370+
schema := definition asJSONSchema.
371+
self assert: schema class equals: JSONSchemaAnyObject.
372+
mediaType := OAMediaTypeObject new schema: schema.
373+
client := ZnClient new.
374+
builder := OARequestBuilder new client: client.
375+
body := Dictionary new at: 'name' put: 'Rex'; yourself.
376+
mediaType writeFormBody: body builder: builder.
377+
self assert: client request entity contentType sub equals: 'x-www-form-urlencoded'.
378+
self assert: (client request entity contents at: 'name') equals: 'Rex'
379+
]

source/OpenAPI-Core/OACookieParameterLocation.class.st

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ Class {
99
{ #category : 'writing' }
1010
OACookieParameterLocation >> write: key value: value to: builder [
1111
"Previously missing entirely - see OAHeaderParametersLocation>>write:value:to:
12-
for the same gap affecting 'in: #cookie' parameters."
13-
builder addCookieParameter: key value: value
12+
for the same gap affecting 'in: #cookie' parameters. Unlike query parameters, a
13+
single Cookie header cannot repeat the same name for form+explode:true arrays -
14+
comma-join instead of passing the raw collection through (the spec leaves this
15+
combination undefined for cookies; this is the pragmatic, documented choice)."
16+
(self serializePairsFor: value name: key defaultStyle: 'form') do: [ :pair | | v |
17+
v := pair value.
18+
builder addCookieParameter: pair key value: (v isString
19+
ifTrue: [ v ]
20+
ifFalse: [ ',' join: v ]) ]
1421
]

source/OpenAPI-Core/OAHeaderParametersLocation.class.st

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,7 @@ OAHeaderParametersLocation >> write: key value: value to: builder [
1717
copyFrom:to: dispatches here for any 'in: #header' parameter, and crashed with
1818
#doesNotUnderstand: #write:value:to: for every client request using a header
1919
parameter (e.g. an API key sent via a custom header)."
20-
builder addHeaderParameter: key value: value
20+
| pairs |
21+
pairs := self serializePairsFor: value name: key defaultStyle: 'simple'.
22+
builder addHeaderParameter: key value: pairs first value
2123
]

0 commit comments

Comments
 (0)