diff --git a/source/OpenAPI-Client-Tests/OARequestBuilderTest.class.st b/source/OpenAPI-Client-Tests/OARequestBuilderTest.class.st index 3ae1123..af38b89 100644 --- a/source/OpenAPI-Client-Tests/OARequestBuilderTest.class.st +++ b/source/OpenAPI-Client-Tests/OARequestBuilderTest.class.st @@ -6,6 +6,16 @@ Class { #tag : 'Tests' } +{ #category : 'tests' } +OARequestBuilderTest >> testAddCookieParameterAccumulatesIntoCookieHeader [ + | client builder | + client := ZnClient new. + builder := OARequestBuilder new client: client. + builder addCookieParameter: 'session' value: 'abc'. + builder addCookieParameter: 'theme' value: 'dark'. + self assert: (client request headers at: 'Cookie') equals: 'session=abc; theme=dark' +] + { #category : 'tests' } OARequestBuilderTest >> testAddFormBodyBuildsFormUrlEncodedEntity [ | client builder entity | @@ -17,6 +27,88 @@ OARequestBuilderTest >> testAddFormBodyBuildsFormUrlEncodedEntity [ self assert: (entity contents at: 'email') equals: 'test@example.com' ] +{ #category : 'tests' } +OARequestBuilderTest >> testAddHeaderParameterSetsRequestHeader [ + "OAHeaderParametersLocation and OACookieParameterLocation used to have no + #write:value:to: method at all - writing any header or cookie parameter into a + real request via OAParameter>>copyFrom:to: crashed with #doesNotUnderstand." + | client builder | + client := ZnClient new. + builder := OARequestBuilder new client: client. + builder addHeaderParameter: 'X-Api-Key' value: 'secret123'. + self assert: (client request headers at: 'X-Api-Key') equals: 'secret123' +] + +{ #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' +] + +{ #category : 'tests' } +OARequestBuilderTest >> testCookieParameterWrittenViaParameterCopyFromTo [ + | param builder client dict | + client := ZnClient new. + param := OAParameter new name: 'session'; in: #cookie; required: true; schema: JSONSchema string; yourself. + builder := OARequestBuilder new client: client. + dict := Dictionary new at: 'session' put: 'abc'; yourself. + param copyFrom: dict to: builder. + self assert: (client request headers at: 'Cookie') equals: 'session=abc' +] + { #category : 'tests' } OARequestBuilderTest >> testFlattenArrayOfObjects [ | builder result items | @@ -76,3 +168,16 @@ OARequestBuilderTest >> testFlattenScalarValues [ self assert: (result at: 'customer') equals: 'cus_123'. self assert: (result at: 'trial_end') equals: 'now' ] + +{ #category : 'tests' } +OARequestBuilderTest >> testHeaderParameterWrittenViaParameterCopyFromTo [ + "End-to-end: a header OAParameter must be writable via the same + #copyFrom:to: path OAOperation>>applyParameters:builder: actually uses." + | param builder client dict | + client := ZnClient new. + param := OAParameter new name: 'X-Api-Key'; in: #header; required: true; schema: JSONSchema string; yourself. + builder := OARequestBuilder new client: client. + dict := Dictionary new at: 'X-Api-Key' put: 'secret123'; yourself. + param copyFrom: dict to: builder. + self assert: (client request headers at: 'X-Api-Key') equals: 'secret123' +] diff --git a/source/OpenAPI-Client/OARequestBuilder.class.st b/source/OpenAPI-Client/OARequestBuilder.class.st index 5945dbf..7164401 100644 --- a/source/OpenAPI-Client/OARequestBuilder.class.st +++ b/source/OpenAPI-Client/OARequestBuilder.class.st @@ -25,8 +25,25 @@ OARequestBuilder >> addJSONBody: aDictionary [ type: ZnMimeType applicationJson setCharSetUTF8) ] +{ #category : 'building' } +OARequestBuilder >> addCookieParameter: key value: value [ + "Multiple cookie parameters must accumulate into a single 'Cookie' header + (name=value pairs joined by '; '), rather than each overwriting the last." + | pair existing | + pair := key asString , '=' , value asString. + existing := client request headers at: 'Cookie' ifAbsent: [ nil ]. + client headerAt: 'Cookie' put: (existing + ifNil: [ pair ] + ifNotNil: [ existing , '; ' , pair ]) +] + +{ #category : 'building' } +OARequestBuilder >> addHeaderParameter: key value: value [ + client headerAt: key put: value asString +] + { #category : 'as yet unclassified' } -OARequestBuilder >> addPathParameter: key value: value [ +OARequestBuilder >> addPathParameter: key value: value [ pathParameters at: key put: value ] diff --git a/source/OpenAPI-Core-Tests/OAExampleDocumentsTests.class.st b/source/OpenAPI-Core-Tests/OAExampleDocumentsTests.class.st new file mode 100644 index 0000000..9b18c5e --- /dev/null +++ b/source/OpenAPI-Core-Tests/OAExampleDocumentsTests.class.st @@ -0,0 +1,61 @@ +Class { + #name : 'OAExampleDocumentsTests', + #superclass : 'TestCase', + #category : 'OpenAPI-Core-Tests', + #package : 'OpenAPI-Core-Tests' +} + +{ #category : 'tests' } +OAExampleDocumentsTests >> testAllBundledExamplesRoundTripExceptUspto [ + "Broader than validity: confirms OpenAPI fromString:/specString handle every bundled + official example without raising, except uspto (see #testUsptoRoundTripFailsOnSchemaTypeSerialization + for the known, not-yet-fixed gap)." + #(petstore petstoreExpanded apiWithExamples callbackExample linkExample) do: [ :selector | + | api | + self shouldnt: [ api := OpenAPI fromString: (OAExampleDocuments perform: selector) ] raise: Error. + self shouldnt: [ api specString ] raise: Error ] +] + +{ #category : 'tests' } +OAExampleDocumentsTests >> testApiWithExamplesIsValid [ + self assert: (OADocumentValidator isValidDocument: OAExampleDocuments apiWithExamples) +] + +{ #category : 'tests' } +OAExampleDocumentsTests >> testCallbackExampleIsValid [ + self assert: (OADocumentValidator isValidDocument: OAExampleDocuments callbackExample) +] + +{ #category : 'tests' } +OAExampleDocumentsTests >> testLinkExampleIsValid [ + self assert: (OADocumentValidator isValidDocument: OAExampleDocuments linkExample) +] + +{ #category : 'tests' } +OAExampleDocumentsTests >> testPetstoreExpandedIsValid [ + self assert: (OADocumentValidator isValidDocument: OAExampleDocuments petstoreExpanded) +] + +{ #category : 'tests' } +OAExampleDocumentsTests >> testPetstoreIsValid [ + self assert: (OADocumentValidator isValidDocument: OAExampleDocuments petstore) +] + +{ #category : 'tests' } +OAExampleDocumentsTests >> testUsptoIsValid [ + self assert: (OADocumentValidator isValidDocument: OAExampleDocuments uspto) +] + +{ #category : 'tests' } +OAExampleDocumentsTests >> testUsptoRoundTripFailsOnSchemaTypeSerialization [ + "Known gap, not fixed here: JSONSchemaDefinition has typeString: (reader) but no + typeString (writer) accessor, so specString cannot re-serialize a schema's type keyword. + Deliberately not fixed in this pass - schemaClass defaults to JSONSchemaAnyObject even + when no type: was ever specified, so a naive typeString getter would wrongly inject + type:any into schemas that never had a type keyword. A correct fix needs to track + whether type was explicitly specified (the same pattern already used for #const / + #constSpecified) before it can round-trip safely." + | api | + api := OpenAPI fromString: OAExampleDocuments uspto. + self should: [ api specString ] raise: MessageNotUnderstood +] diff --git a/source/OpenAPI-Core/OACookieParameterLocation.class.st b/source/OpenAPI-Core/OACookieParameterLocation.class.st index ef1df19..ade5088 100644 --- a/source/OpenAPI-Core/OACookieParameterLocation.class.st +++ b/source/OpenAPI-Core/OACookieParameterLocation.class.st @@ -5,3 +5,10 @@ Class { #package : 'OpenAPI-Core', #tag : 'Model' } + +{ #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 +] diff --git a/source/OpenAPI-Core/OAExampleDocuments.class.st b/source/OpenAPI-Core/OAExampleDocuments.class.st new file mode 100644 index 0000000..ae6593d --- /dev/null +++ b/source/OpenAPI-Core/OAExampleDocuments.class.st @@ -0,0 +1,47 @@ +Class { + #name : 'OAExampleDocuments', + #superclass : 'Object', + #category : 'OpenAPI-Core', + #package : 'OpenAPI-Core' +} + +{ #category : 'documents' } +OAExampleDocuments class >> apiWithExamples [ + "Official OAI 3.0 example (see #attribution)." + ^ '{"openapi":"3.0.0","info":{"title":"Simple API overview","version":"2.0.0"},"paths":{"/":{"get":{"operationId":"listVersionsv2","summary":"List API versions","responses":{"200":{"description":"200 response","content":{"application/json":{"examples":{"foo":{"value":{"versions":[{"status":"CURRENT","updated":"2011-01-21T11:33:21Z","id":"v2.0","links":[{"href":"http://127.0.0.1:8774/v2/","rel":"self"}]},{"status":"EXPERIMENTAL","updated":"2013-07-23T11:33:21Z","id":"v3.0","links":[{"href":"http://127.0.0.1:8774/v3/","rel":"self"}]}]}}}}}},"300":{"description":"300 response","content":{"application/json":{"examples":{"foo":{"value":"{\n \"versions\": [\n {\n \"status\": \"CURRENT\",\n \"updated\": \"2011-01-21T11:33:21Z\",\n \"id\": \"v2.0\",\n \"links\": [\n {\n \"href\": \"http://127.0.0.1:8774/v2/\",\n \"rel\": \"self\"\n }\n ]\n },\n {\n \"status\": \"EXPERIMENTAL\",\n \"updated\": \"2013-07-23T11:33:21Z\",\n \"id\": \"v3.0\",\n \"links\": [\n {\n \"href\": \"http://127.0.0.1:8774/v3/\",\n \"rel\": \"self\"\n }\n ]\n }\n ]\n}\n"}}}}}}}},"/v2":{"get":{"operationId":"getVersionDetailsv2","summary":"Show API version details","responses":{"200":{"description":"200 response","content":{"application/json":{"examples":{"foo":{"value":{"version":{"status":"CURRENT","updated":"2011-01-21T11:33:21Z","media-types":[{"base":"application/xml","type":"application/vnd.openstack.compute+xml;version=2"},{"base":"application/json","type":"application/vnd.openstack.compute+json;version=2"}],"id":"v2.0","links":[{"href":"http://127.0.0.1:8774/v2/","rel":"self"},{"href":"http://docs.openstack.org/api/openstack-compute/2/os-compute-devguide-2.pdf","type":"application/pdf","rel":"describedby"},{"href":"http://docs.openstack.org/api/openstack-compute/2/wadl/os-compute-2.wadl","type":"application/vnd.sun.wadl+xml","rel":"describedby"},{"href":"http://docs.openstack.org/api/openstack-compute/2/wadl/os-compute-2.wadl","type":"application/vnd.sun.wadl+xml","rel":"describedby"}]}}}}}}},"203":{"description":"203 response","content":{"application/json":{"examples":{"foo":{"value":{"version":{"status":"CURRENT","updated":"2011-01-21T11:33:21Z","media-types":[{"base":"application/xml","type":"application/vnd.openstack.compute+xml;version=2"},{"base":"application/json","type":"application/vnd.openstack.compute+json;version=2"}],"id":"v2.0","links":[{"href":"http://23.253.228.211:8774/v2/","rel":"self"},{"href":"http://docs.openstack.org/api/openstack-compute/2/os-compute-devguide-2.pdf","type":"application/pdf","rel":"describedby"},{"href":"http://docs.openstack.org/api/openstack-compute/2/wadl/os-compute-2.wadl","type":"application/vnd.sun.wadl+xml","rel":"describedby"}]}}}}}}}}}}}}' +] + +{ #category : 'accessing' } +OAExampleDocuments class >> attribution [ + ^ 'Example OpenAPI 3.0 documents bundled verbatim (YAML converted to JSON, whitespace-compacted) from the official OAI/OpenAPI-Specification repository (github.com/OAI/OpenAPI-Specification, _archive_/schemas/v3.0/pass/), licensed under Apache-2.0. These are the same documents the OAI project itself uses to verify its 3.0 meta-schema accepts real, non-trivial specs.' +] + +{ #category : 'documents' } +OAExampleDocuments class >> callbackExample [ + "Official OAI 3.0 example (see #attribution)." + ^ '{"openapi":"3.0.0","info":{"title":"Callback Example","version":"1.0.0"},"paths":{"/streams":{"post":{"description":"subscribes a client to receive out-of-band data","parameters":[{"name":"callbackUrl","in":"query","required":true,"description":"the location where data will be sent. Must be network accessible\nby the source server\n","schema":{"type":"string","format":"uri","example":"https://tonys-server.com"}}],"responses":{"201":{"description":"subscription successfully created","content":{"application/json":{"schema":{"description":"subscription information","required":["subscriptionId"],"properties":{"subscriptionId":{"description":"this unique identifier allows management of the subscription","type":"string","example":"2531329f-fb09-4ef7-887e-84e648214436"}}}}}}},"callbacks":{"onData":{"{$request.query.callbackUrl}/data":{"post":{"requestBody":{"description":"subscription payload","content":{"application/json":{"schema":{"type":"object","properties":{"timestamp":{"type":"string","format":"date-time"},"userData":{"type":"string"}}}}}},"responses":{"202":{"description":"Your server implementation should return this HTTP status code\nif the data was received successfully\n"},"204":{"description":"Your server should return this HTTP status code if no longer interested\nin further updates\n"}}}}}}}}}}' +] + +{ #category : 'documents' } +OAExampleDocuments class >> linkExample [ + "Official OAI 3.0 example (see #attribution)." + ^ '{"openapi":"3.0.0","info":{"title":"Link Example","version":"1.0.0"},"paths":{"/2.0/users/{username}":{"get":{"operationId":"getUserByName","parameters":[{"name":"username","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The User","content":{"application/json":{"schema":{"$ref":"#/components/schemas/user"}}},"links":{"userRepositories":{"$ref":"#/components/links/UserRepositories"}}}}}},"/2.0/repositories/{username}":{"get":{"operationId":"getRepositoriesByOwner","parameters":[{"name":"username","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"repositories owned by the supplied user","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/repository"}}}},"links":{"userRepository":{"$ref":"#/components/links/UserRepository"}}}}}},"/2.0/repositories/{username}/{slug}":{"get":{"operationId":"getRepository","parameters":[{"name":"username","in":"path","required":true,"schema":{"type":"string"}},{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The repository","content":{"application/json":{"schema":{"$ref":"#/components/schemas/repository"}}},"links":{"repositoryPullRequests":{"$ref":"#/components/links/RepositoryPullRequests"}}}}}},"/2.0/repositories/{username}/{slug}/pullrequests":{"get":{"operationId":"getPullRequestsByRepository","parameters":[{"name":"username","in":"path","required":true,"schema":{"type":"string"}},{"name":"slug","in":"path","required":true,"schema":{"type":"string"}},{"name":"state","in":"query","schema":{"type":"string","enum":["open","merged","declined"]}}],"responses":{"200":{"description":"an array of pull request objects","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/pullrequest"}}}}}}}},"/2.0/repositories/{username}/{slug}/pullrequests/{pid}":{"get":{"operationId":"getPullRequestsById","parameters":[{"name":"username","in":"path","required":true,"schema":{"type":"string"}},{"name":"slug","in":"path","required":true,"schema":{"type":"string"}},{"name":"pid","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"a pull request object","content":{"application/json":{"schema":{"$ref":"#/components/schemas/pullrequest"}}},"links":{"pullRequestMerge":{"$ref":"#/components/links/PullRequestMerge"}}}}}},"/2.0/repositories/{username}/{slug}/pullrequests/{pid}/merge":{"post":{"operationId":"mergePullRequest","parameters":[{"name":"username","in":"path","required":true,"schema":{"type":"string"}},{"name":"slug","in":"path","required":true,"schema":{"type":"string"}},{"name":"pid","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"the PR was successfully merged"}}}}},"components":{"links":{"UserRepositories":{"operationId":"getRepositoriesByOwner","parameters":{"username":"$response.body#/username"}},"UserRepository":{"operationId":"getRepository","parameters":{"username":"$response.body#/owner/username","slug":"$response.body#/slug"}},"RepositoryPullRequests":{"operationId":"getPullRequestsByRepository","parameters":{"username":"$response.body#/owner/username","slug":"$response.body#/slug"}},"PullRequestMerge":{"operationId":"mergePullRequest","parameters":{"username":"$response.body#/author/username","slug":"$response.body#/repository/slug","pid":"$response.body#/id"}}},"schemas":{"user":{"type":"object","properties":{"username":{"type":"string"},"uuid":{"type":"string"}}},"repository":{"type":"object","properties":{"slug":{"type":"string"},"owner":{"$ref":"#/components/schemas/user"}}},"pullrequest":{"type":"object","properties":{"id":{"type":"integer"},"title":{"type":"string"},"repository":{"$ref":"#/components/schemas/repository"},"author":{"$ref":"#/components/schemas/user"}}}}}}' +] + +{ #category : 'documents' } +OAExampleDocuments class >> petstore [ + "Official OAI 3.0 example (see #attribution)." + ^ '{"openapi":"3.0.0","info":{"version":"1.0.0","title":"Swagger Petstore","license":{"name":"MIT"}},"servers":[{"url":"http://petstore.swagger.io/v1"}],"paths":{"/pets":{"get":{"summary":"List all pets","operationId":"listPets","tags":["pets"],"parameters":[{"name":"limit","in":"query","description":"How many items to return at one time (max 100)","required":false,"schema":{"type":"integer","maximum":100,"format":"int32"}}],"responses":{"200":{"description":"A paged array of pets","headers":{"x-next":{"description":"A link to the next page of responses","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pets"}}}},"default":{"description":"unexpected error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"summary":"Create a pet","operationId":"createPets","tags":["pets"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pet"}}},"required":true},"responses":{"201":{"description":"Null response"},"default":{"description":"unexpected error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/pets/{petId}":{"get":{"summary":"Info for a specific pet","operationId":"showPetById","tags":["pets"],"parameters":[{"name":"petId","in":"path","required":true,"description":"The id of the pet to retrieve","schema":{"type":"string"}}],"responses":{"200":{"description":"Expected response to a valid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pet"}}}},"default":{"description":"unexpected error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"schemas":{"Pet":{"type":"object","required":["id","name"],"properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"tag":{"type":"string"}}},"Pets":{"type":"array","maxItems":100,"items":{"$ref":"#/components/schemas/Pet"}},"Error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"integer","format":"int32"},"message":{"type":"string"}}}}}}' +] + +{ #category : 'documents' } +OAExampleDocuments class >> petstoreExpanded [ + "Official OAI 3.0 example (see #attribution)." + ^ '{"openapi":"3.0.0","info":{"version":"1.0.0","title":"Swagger Petstore","description":"A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification","termsOfService":"http://swagger.io/terms/","contact":{"name":"Swagger API Team","email":"apiteam@swagger.io","url":"http://swagger.io"},"license":{"name":"Apache 2.0","url":"https://www.apache.org/licenses/LICENSE-2.0.html"}},"servers":[{"url":"https://petstore.swagger.io/v2"}],"paths":{"/pets":{"get":{"description":"Returns all pets from the system that the user has access to\nNam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia.\n\nSed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien.\n","operationId":"findPets","parameters":[{"name":"tags","in":"query","description":"tags to filter by","required":false,"style":"form","schema":{"type":"array","items":{"type":"string"}}},{"name":"limit","in":"query","description":"maximum number of results to return","required":false,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"pet response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Pet"}}}}},"default":{"description":"unexpected error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"description":"Creates a new pet in the store. Duplicates are allowed","operationId":"addPet","requestBody":{"description":"Pet to add to the store","required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewPet"}}}},"responses":{"200":{"description":"pet response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pet"}}}},"default":{"description":"unexpected error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/pets/{id}":{"get":{"description":"Returns a user based on a single ID, if the user does not have access to the pet","operationId":"find pet by id","parameters":[{"name":"id","in":"path","description":"ID of pet to fetch","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"200":{"description":"pet response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pet"}}}},"default":{"description":"unexpected error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"description":"deletes a single pet based on the ID supplied","operationId":"deletePet","parameters":[{"name":"id","in":"path","description":"ID of pet to delete","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"204":{"description":"pet deleted"},"default":{"description":"unexpected error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"schemas":{"Pet":{"allOf":[{"$ref":"#/components/schemas/NewPet"},{"type":"object","required":["id"],"properties":{"id":{"type":"integer","format":"int64"}}}]},"NewPet":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"tag":{"type":"string"}}},"Error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"integer","format":"int32"},"message":{"type":"string"}}}}}}' +] + +{ #category : 'documents' } +OAExampleDocuments class >> uspto [ + "Official OAI 3.0 example (see #attribution)." + ^ '{"openapi":"3.0.1","servers":[{"url":"{scheme}://developer.uspto.gov/ds-api","variables":{"scheme":{"description":"The Data Set API is accessible via https and http","enum":["https","http"],"default":"https"}}}],"info":{"description":"The Data Set API (DSAPI) allows the public users to discover and search USPTO exported data sets. This is a generic API that allows USPTO users to make any CSV based data files searchable through API. With the help of GET call, it returns the list of data fields that are searchable. With the help of POST call, data can be fetched based on the filters on the field names. Please note that POST call is used to search the actual data. The reason for the POST call is that it allows users to specify any complex search criteria without worry about the GET size limitations as well as encoding of the input parameters.","version":"1.0.0","title":"USPTO Data Set API","contact":{"name":"Open Data Portal","url":"https://developer.uspto.gov","email":"developer@uspto.gov"}},"tags":[{"name":"metadata","description":"Find out about the data sets"},{"name":"search","description":"Search a data set"}],"paths":{"/":{"get":{"tags":["metadata"],"operationId":"list-data-sets","summary":"List available data sets","responses":{"200":{"description":"Returns a list of data sets","content":{"application/json":{"schema":{"$ref":"#/components/schemas/dataSetList"},"example":{"total":2,"apis":[{"apiKey":"oa_citations","apiVersionNumber":"v1","apiUrl":"https://developer.uspto.gov/ds-api/oa_citations/v1/fields","apiDocumentationUrl":"https://developer.uspto.gov/ds-api-docs/index.html?url=https://developer.uspto.gov/ds-api/swagger/docs/oa_citations.json"},{"apiKey":"cancer_moonshot","apiVersionNumber":"v1","apiUrl":"https://developer.uspto.gov/ds-api/cancer_moonshot/v1/fields","apiDocumentationUrl":"https://developer.uspto.gov/ds-api-docs/index.html?url=https://developer.uspto.gov/ds-api/swagger/docs/cancer_moonshot.json"}]}}}}}}},"/{dataset}/{version}/fields":{"get":{"tags":["metadata"],"summary":"Provides the general information about the API and the list of fields that can be used to query the dataset.","description":"This GET API returns the list of all the searchable field names that are in the oa_citations. Please see the ''fields'' attribute which returns an array of field names. Each field or a combination of fields can be searched using the syntax options shown below.","operationId":"list-searchable-fields","parameters":[{"name":"dataset","in":"path","description":"Name of the dataset.","required":true,"example":"oa_citations","schema":{"type":"string"}},{"name":"version","in":"path","description":"Version of the dataset.","required":true,"example":"v1","schema":{"type":"string"}}],"responses":{"200":{"description":"The dataset API for the given version is found and it is accessible to consume.","content":{"application/json":{"schema":{"type":"string"}}}},"404":{"description":"The combination of dataset name and version is not found in the system or it is not published yet to be consumed by public.","content":{"application/json":{"schema":{"type":"string"}}}}}}},"/{dataset}/{version}/records":{"post":{"tags":["search"],"summary":"Provides search capability for the data set with the given search criteria.","description":"This API is based on Solr/Lucene Search. The data is indexed using SOLR. This GET API returns the list of all the searchable field names that are in the Solr Index. Please see the ''fields'' attribute which returns an array of field names. Each field or a combination of fields can be searched using the Solr/Lucene Syntax. Please refer https://lucene.apache.org/core/3_6_2/queryparsersyntax.html#Overview for the query syntax. List of field names that are searchable can be determined using above GET api.","operationId":"perform-search","parameters":[{"name":"version","in":"path","description":"Version of the dataset.","required":true,"schema":{"type":"string","default":"v1"}},{"name":"dataset","in":"path","description":"Name of the dataset. In this case, the default value is oa_citations","required":true,"schema":{"type":"string","default":"oa_citations"}}],"responses":{"200":{"description":"successful operation","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":{"type":"object"}}}}}},"404":{"description":"No matching record found for the given criteria."}},"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"criteria":{"description":"Uses Lucene Query Syntax in the format of propertyName:value, propertyName:[num1 TO num2] and date range format: propertyName:[yyyyMMdd TO yyyyMMdd]. In the response please see the ''docs'' element which has the list of record objects. Each record structure would consist of all the fields and their corresponding values.","type":"string","default":"*:*"},"start":{"description":"Starting record number. Default value is 0.","type":"integer","default":0},"rows":{"description":"Specify number of rows to be returned. If you run the search with default values, in the response you will see ''numFound'' attribute which will tell the number of records available in the dataset.","type":"integer","default":100}},"required":["criteria"]}}}}}}},"components":{"schemas":{"dataSetList":{"type":"object","properties":{"total":{"type":"integer"},"apis":{"type":"array","items":{"type":"object","properties":{"apiKey":{"type":"string","description":"To be used as a dataset parameter value"},"apiVersionNumber":{"type":"string","description":"To be used as a version parameter value"},"apiUrl":{"type":"string","format":"uriref","description":"The URL describing the dataset''s fields"},"apiDocumentationUrl":{"type":"string","format":"uriref","description":"A URL to the API console for each API"}}}}}}}}}' +] diff --git a/source/OpenAPI-Core/OAHeaderParametersLocation.class.st b/source/OpenAPI-Core/OAHeaderParametersLocation.class.st index 2f109ff..f064f6c 100644 --- a/source/OpenAPI-Core/OAHeaderParametersLocation.class.st +++ b/source/OpenAPI-Core/OAHeaderParametersLocation.class.st @@ -10,3 +10,12 @@ Class { OAHeaderParametersLocation >> extractParameter: aCall [ ^ parameter read: (aCall request headers at: parameter name ifAbsent: [ ^ nil ]) ] + +{ #category : 'writing' } +OAHeaderParametersLocation >> write: key value: value to: builder [ + "Previously missing entirely: OAOperation>>applyParameters:builder: -> OAParameter>> + 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 +] diff --git a/source/OpenAPI-Core/OALink.class.st b/source/OpenAPI-Core/OALink.class.st index 4a6f0e5..d47d70b 100644 --- a/source/OpenAPI-Core/OALink.class.st +++ b/source/OpenAPI-Core/OALink.class.st @@ -1,7 +1,85 @@ Class { #name : 'OALink', #superclass : 'OABasicObject', + #instVars : [ + 'operationId', + 'operationRef', + 'parameters', + 'requestBody', + 'description', + 'server' + ], #category : 'OpenAPI-Core-Model', #package : 'OpenAPI-Core', #tag : 'Model' } + +{ #category : 'instance creation' } +OALink class >> neoJsonMapping: mapper [ + "operationId/operationRef are mutually exclusive per spec (not enforced here - that is + OADocumentValidator/the meta-schema's job). parameters/requestBody/server are raw, + passed-through JSON data (parameters values are runtime expression strings; requestBody + is any; server is a plain Server Object with no dedicated OA* class in this codebase)." + mapper for: self do: [ :mapping | + mapping mapAccessors: #( operationId operationRef parameters requestBody description server ) ] +] + +{ #category : 'accessing' } +OALink >> description [ + ^ description +] + +{ #category : 'accessing' } +OALink >> description: anObject [ + description := anObject +] + +{ #category : 'accessing' } +OALink >> operationId [ + ^ operationId +] + +{ #category : 'accessing' } +OALink >> operationId: anObject [ + operationId := anObject +] + +{ #category : 'accessing' } +OALink >> operationRef [ + ^ operationRef +] + +{ #category : 'accessing' } +OALink >> operationRef: anObject [ + operationRef := anObject +] + +{ #category : 'accessing' } +OALink >> parameters [ + ^ parameters +] + +{ #category : 'accessing' } +OALink >> parameters: anObject [ + parameters := anObject +] + +{ #category : 'accessing' } +OALink >> requestBody [ + ^ requestBody +] + +{ #category : 'accessing' } +OALink >> requestBody: anObject [ + requestBody := anObject +] + +{ #category : 'accessing' } +OALink >> server [ + ^ server +] + +{ #category : 'accessing' } +OALink >> server: anObject [ + server := anObject +] diff --git a/source/OpenAPI-Core/OAMediaTypeObject.class.st b/source/OpenAPI-Core/OAMediaTypeObject.class.st index 290842c..7d47244 100644 --- a/source/OpenAPI-Core/OAMediaTypeObject.class.st +++ b/source/OpenAPI-Core/OAMediaTypeObject.class.st @@ -18,10 +18,12 @@ OAMediaTypeObject class >> neoJsonMapping: mapper [ mapper for: self do: [ :mapping | mapping mapAccessors: #( encoding ). (mapping mapAccessor: #schema) valueSchema: OASchemaDefinition. - (mapping mapAccessor: #example) valueSchema: OAExample. + "example (unlike a named entry in #examples) is a raw, arbitrary value per spec - not an + Example Object - so no valueSchema: it maps straight through as JSON data." + mapping mapAccessor: #example. (mapping mapAccessor: #examples) valueSchema: #OAExamples ]. mapper for: #OAExamples customDo: [ :mapping | - mapping mapWithValueSchema: OAExample ] + mapping mapWithValueSchema: OAExample ] ] { #category : 'visting' } @@ -49,6 +51,11 @@ OAMediaTypeObject >> example [ ^ example ] +{ #category : 'accessing' } +OAMediaTypeObject >> example: anObject [ + example := anObject +] + { #category : 'as yet unclassified' } OAMediaTypeObject >> examples [ ^ examples @@ -107,16 +114,23 @@ OAMediaTypeObject >> write: anObject [ ] { #category : 'as yet unclassified' } -OAMediaTypeObject >> writeBody: aDictionary builder: builder [ - builder addJSONBody: (schema isAnyObject - ifTrue: [ aDictionary ] - ifFalse: [ schema readObject: aDictionary ]) +OAMediaTypeObject >> writeBody: aDictionary builder: builder [ + "#isAnyObject is only implemented on JSONSchemaObject - any non-object body + schema (bare string/array, or an allOf-composed schema which resolves to + JSONSchemaAnyObject since nothing sets schemaClass for a bare allOf) used to + crash here with #doesNotUnderstand: #isAnyObject. Such schemas degrade to a + plain passthrough, same as the JSONSchemaObject-with-no-properties case." + builder addJSONBody: (((schema isKindOf: JSONSchemaObject) and: [ schema isAnyObject not ]) + ifTrue: [ schema readObject: aDictionary ] + ifFalse: [ aDictionary ]) ] { #category : 'as yet unclassified' } OAMediaTypeObject >> writeFormBody: aDictionary builder: builder [ - builder addFormBody: (schema isAnyObject - ifTrue: [ aDictionary ] - ifFalse: [ schema readObject: aDictionary ]) + "See #writeBody:builder: for why this checks (schema isKindOf: JSONSchemaObject) + instead of unconditionally sending #isAnyObject." + builder addFormBody: (((schema isKindOf: JSONSchemaObject) and: [ schema isAnyObject not ]) + ifTrue: [ schema readObject: aDictionary ] + ifFalse: [ aDictionary ]) ] diff --git a/source/OpenAPI-Core/OAReferenceResolveVisitor.class.st b/source/OpenAPI-Core/OAReferenceResolveVisitor.class.st index cdd6ab9..f5985f0 100644 --- a/source/OpenAPI-Core/OAReferenceResolveVisitor.class.st +++ b/source/OpenAPI-Core/OAReferenceResolveVisitor.class.st @@ -52,12 +52,14 @@ OAReferenceResolveVisitor >> visit: anObject [ { #category : 'as yet unclassified' } OAReferenceResolveVisitor >> visitOpenApi: anApi [ -| visitor | + "components is optional per spec; a document without it (or without components.schemas) has nothing to resolve here." + | visitor | visitor := JSONSchemaReferenceResolveVisitor new schemaRepository: self. - anApi components schemas: (anApi components schemas collect: #asJSONSchema). - anApi components schemas: (anApi components schemas collect: [ :definition | - visitor read: definition ]). + (anApi components notNil and: [ anApi components schemas notNil ]) ifTrue: [ + anApi components schemas: (anApi components schemas collect: #asJSONSchema). + anApi components schemas: (anApi components schemas collect: [ :definition | + visitor read: definition ]) ]. ^ super visitOpenApi: anApi ] diff --git a/source/OpenAPI-REST-Tests/OpenAPIRestTests.class.st b/source/OpenAPI-REST-Tests/OpenAPIRestTests.class.st index 5f06ac0..23ab84a 100644 --- a/source/OpenAPI-REST-Tests/OpenAPIRestTests.class.st +++ b/source/OpenAPI-REST-Tests/OpenAPIRestTests.class.st @@ -29,3 +29,12 @@ OpenAPIRestTests >> runCase [ value: self during: [ super runCase ] ] + +{ #category : 'running' } +OpenAPIRestTests >> setUp [ + "Pet instances accumulates in a class-side IdentitySet with no cleanup - across + repeated test runs within the same image this grows without bound. Reset it + before each test so tests stay isolated from each other." + super setUp. + Pet resetInstances +]