diff --git a/README.md b/README.md index 5e751dc8..850de813 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,9 @@ it's completely independent from it, so anybody can use it. Skeuomorph depends heavily on [cats][] and [droste][]. +## NOTICE +The following files `api-with-examples.yaml`, `petstore-expanded.yaml`, `callback-example.yaml`, `petstore.yaml`, `link-example.yaml` and `uspto.yaml` inside the folder (`test/resources/openapi/yaml`) were copied from [**OpenAPI Specification**](https://github.com/OAI/OpenAPI-Specification/) project under the terms of the licence [*Apache License Version 2.0, January 2004*](https://github.com/OAI/OpenAPI-Specification/blob/master/LICENSE). + ## Schemas Currently skeuomorph supports 3 different schemas: diff --git a/build.sbt b/build.sbt index 1b4106cb..9b208f85 100644 --- a/build.sbt +++ b/build.sbt @@ -98,12 +98,13 @@ lazy val commonSettings = Seq( libraryDependencies ++= Seq( %%("cats-laws", V.cats) % Test, %%("cats-core", V.cats), - "io.higherkindness" %% "droste-core" % V.droste, - "io.higherkindness" %% "droste-macros" % V.droste, - "org.apache.avro" % "avro" % V.avro, - "com.github.os72" % "protoc-jar" % V.protoc, - "com.google.protobuf" % "protobuf-java" % V.protobuf, - "io.circe" %% "circe-testing" % V.circe % Test, + "io.higherkindness" %% "droste-core" % V.droste, + "io.higherkindness" %% "droste-macros" % V.droste, + "org.apache.avro" % "avro" % V.avro, + "com.github.os72" % "protoc-jar" % V.protoc, + "com.google.protobuf" % "protobuf-java" % V.protobuf, + "io.circe" %% "circe-yaml" % "0.10.0", + "io.circe" %% "circe-testing" % V.circe % Test, %%("cats-effect", V.catsEffect), %%("circe-core", V.circe), %%("circe-parser", V.circe) % Test, diff --git a/src/main/scala/higherkindness/skeuomorph/openapi/JsonDecoders.scala b/src/main/scala/higherkindness/skeuomorph/openapi/JsonDecoders.scala index a5dba317..c6a2a754 100644 --- a/src/main/scala/higherkindness/skeuomorph/openapi/JsonDecoders.scala +++ b/src/main/scala/higherkindness/skeuomorph/openapi/JsonDecoders.scala @@ -53,7 +53,7 @@ object JsonDecoders { private def validateType(c: HCursor, expected: String): Decoder.Result[Unit] = c.downField("type").as[String].flatMap { case `expected` => ().asRight - case actual => DecodingFailure(s"$actual is not expected type $expected", List.empty).asLeft + case actual => DecodingFailure(s"$actual is not expected type $expected", c.history).asLeft } private def enumJsonSchemaDecoder[A: Embed[JsonSchemaF, ?]]: Decoder[A] = @@ -65,7 +65,17 @@ object JsonDecoders { private def objectJsonSchemaDecoder[A: Embed[JsonSchemaF, ?]]: Decoder[A] = Decoder.instance { c => + def propertyExists(name: String): Decoder.Result[Unit] = + c.downField(name) + .success + .fold(DecodingFailure(s"$name property does not exist", c.history).asLeft[Unit])(_ => + ().asRight[DecodingFailure]) + def isObject: Decoder.Result[Unit] = + validateType(c, "object") orElse + propertyExists("properties") orElse + propertyExists("allOf") for { + _ <- isObject required <- c.downField("required").as[Option[List[String]]] properties <- c .downField("properties") @@ -74,7 +84,6 @@ object JsonDecoders { ) .map(_.getOrElse(Map.empty)) .map(_.toList.map(JsonSchemaF.Property.apply[A] _ tupled)) - _ <- validateType(c, "object") orElse c.downField("properties").as[Map[String, A]].map(_ => ()) } yield JsonSchemaF.`object`[A](properties, required.getOrElse(List.empty)).embed } @@ -271,14 +280,16 @@ object JsonDecoders { servers.getOrElse(List.empty)))) implicit def componentsDecoder[A: Decoder]: Decoder[Components[A]] = - Decoder.forProduct2( + Decoder.forProduct3( + "schemas", "responses", "requestBodies" )( ( + schemas: Option[Map[String, A]], responses: Option[Map[String, Either[Response[A], Reference]]], requestBodies: Option[Map[String, Either[Request[A], Reference]]]) => - Components(responses.getOrElse(Map.empty), requestBodies.getOrElse(Map.empty))) + Components(schemas.getOrElse(Map.empty), responses.getOrElse(Map.empty), requestBodies.getOrElse(Map.empty))) implicit def openApiDecoder[A: Decoder]: Decoder[OpenApi[A]] = Decoder.forProduct7( diff --git a/src/main/scala/higherkindness/skeuomorph/openapi/JsonEncoders.scala b/src/main/scala/higherkindness/skeuomorph/openapi/JsonEncoders.scala index afc5decc..182e90da 100644 --- a/src/main/scala/higherkindness/skeuomorph/openapi/JsonEncoders.scala +++ b/src/main/scala/higherkindness/skeuomorph/openapi/JsonEncoders.scala @@ -181,10 +181,11 @@ object JsonEncoders { } implicit def componentsEncoder[A: Encoder]: Encoder[Components[A]] = - Encoder.forProduct2( + Encoder.forProduct3( + "schemas", "responses", "requestBodies" - )(c => (c.responses, c.requestBodies)) + )(c => (c.schemas, c.responses, c.requestBodies)) implicit def openApiEncoder[A: Encoder]: Encoder[OpenApi[A]] = Encoder.forProduct7( diff --git a/src/main/scala/higherkindness/skeuomorph/openapi/schema.scala b/src/main/scala/higherkindness/skeuomorph/openapi/schema.scala index c796cd65..ed49b21f 100644 --- a/src/main/scala/higherkindness/skeuomorph/openapi/schema.scala +++ b/src/main/scala/higherkindness/skeuomorph/openapi/schema.scala @@ -73,6 +73,7 @@ object schema { servers: List[Server]) } final case class Components[A]( + schemas: Map[String, A], responses: Map[String, Either[Response[A], Reference]], requestBodies: Map[String, Either[Request[A], Reference]] ) diff --git a/src/main/scala/higherkindness/skeuomorph/openapi/yaml.scala b/src/main/scala/higherkindness/skeuomorph/openapi/yaml.scala new file mode 100644 index 00000000..2fc2a82d --- /dev/null +++ b/src/main/scala/higherkindness/skeuomorph/openapi/yaml.scala @@ -0,0 +1,32 @@ +/* + * Copyright 2018-2019 47 Degrees, LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package higherkindness.skeuomorph.openapi +import io.circe.{Decoder => JsonDecoder, _} +import io.circe.yaml.parser +import cats.implicits._ + +object yaml { + type Failure = Either[ParsingFailure, DecodingFailure] + type Decoder[A] = String => Either[Failure, A] + + object Decoder { + def apply[A](implicit decoder: Decoder[A]): Decoder[A] = decoder + } + implicit def fromJsonDecoder[A: JsonDecoder]: Decoder[A] = + parser.parse(_).leftMap(_.asLeft).flatMap(JsonDecoder[A].decodeJson(_).leftMap(_.asRight)) + +} diff --git a/src/test/resources/api-with-examples.json b/src/test/resources/openapi/json/api-with-examples.json similarity index 100% rename from src/test/resources/api-with-examples.json rename to src/test/resources/openapi/json/api-with-examples.json diff --git a/src/test/resources/callback-example.json b/src/test/resources/openapi/json/callback-example.json similarity index 100% rename from src/test/resources/callback-example.json rename to src/test/resources/openapi/json/callback-example.json diff --git a/src/test/resources/link-example.json b/src/test/resources/openapi/json/link-example.json similarity index 100% rename from src/test/resources/link-example.json rename to src/test/resources/openapi/json/link-example.json diff --git a/src/test/resources/petstore-expanded.json b/src/test/resources/openapi/json/petstore-expanded.json similarity index 100% rename from src/test/resources/petstore-expanded.json rename to src/test/resources/openapi/json/petstore-expanded.json diff --git a/src/test/resources/petstore.json b/src/test/resources/openapi/json/petstore.json similarity index 100% rename from src/test/resources/petstore.json rename to src/test/resources/openapi/json/petstore.json diff --git a/src/test/resources/uspto.json b/src/test/resources/openapi/json/uspto.json similarity index 100% rename from src/test/resources/uspto.json rename to src/test/resources/openapi/json/uspto.json diff --git a/src/test/resources/openapi/yaml/api-with-examples.yaml b/src/test/resources/openapi/yaml/api-with-examples.yaml new file mode 100755 index 00000000..09003b6a --- /dev/null +++ b/src/test/resources/openapi/yaml/api-with-examples.yaml @@ -0,0 +1,167 @@ +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: | + { + "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" + } + ] + } + ] + } + /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" + } + ] + } + } diff --git a/src/test/resources/openapi/yaml/callback-example.yaml b/src/test/resources/openapi/yaml/callback-example.yaml new file mode 100755 index 00000000..1622bd06 --- /dev/null +++ b/src/test/resources/openapi/yaml/callback-example.yaml @@ -0,0 +1,60 @@ +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 + by the source server + 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: + # the name `onData` is a convenience locator + onData: + # when data is sent, it will be sent to the `callbackUrl` provided + # when making the subscription PLUS the suffix `/data` + '{$request.query.callbackUrl}/data': + post: + requestBody: + description: subscription payload + content: + application/json: + schema: + properties: + timestamp: + type: string + format: date-time + userData: + type: string + responses: + '202': + description: | + Your server implementation should return this HTTP status code + if the data was received successfully + '204': + description: | + Your server should return this HTTP status code if no longer interested + in further updates diff --git a/src/test/resources/openapi/yaml/link-example.yaml b/src/test/resources/openapi/yaml/link-example.yaml new file mode 100755 index 00000000..5837d705 --- /dev/null +++ b/src/test/resources/openapi/yaml/link-example.yaml @@ -0,0 +1,203 @@ +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: + # returns array of '#/components/schemas/repository' + operationId: getRepositoriesByOwner + parameters: + username: $response.body#/username + UserRepository: + # returns '#/components/schemas/repository' + operationId: getRepository + parameters: + username: $response.body#/owner/username + slug: $response.body#/slug + RepositoryPullRequests: + # returns '#/components/schemas/pullrequest' + operationId: getPullRequestsByRepository + parameters: + username: $response.body#/owner/username + slug: $response.body#/slug + PullRequestMerge: + # executes /2.0/repositories/{username}/{slug}/pullrequests/{pid}/merge + 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' diff --git a/src/test/resources/openapi/yaml/petstore-expanded.yaml b/src/test/resources/openapi/yaml/petstore-expanded.yaml new file mode 100755 index 00000000..d7533318 --- /dev/null +++ b/src/test/resources/openapi/yaml/petstore-expanded.yaml @@ -0,0 +1,155 @@ +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: http://petstore.swagger.io/api +paths: + /pets: + get: + description: | + Returns all pets from the system that the user has access to + Nam 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. + + Sed 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. + 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' + - required: + - id + properties: + id: + type: integer + format: int64 + + NewPet: + required: + - name + properties: + name: + type: string + tag: + type: string + + Error: + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string diff --git a/src/test/resources/openapi/yaml/petstore.yaml b/src/test/resources/openapi/yaml/petstore.yaml new file mode 100755 index 00000000..09941de9 --- /dev/null +++ b/src/test/resources/openapi/yaml/petstore.yaml @@ -0,0 +1,109 @@ +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 + 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 + 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/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" +components: + schemas: + Pet: + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: "#/components/schemas/Pet" + Error: + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string diff --git a/src/test/resources/openapi/yaml/uspto.yaml b/src/test/resources/openapi/yaml/uspto.yaml new file mode 100755 index 00000000..8e9b159f --- /dev/null +++ b/src/test/resources/openapi/yaml/uspto.yaml @@ -0,0 +1,210 @@ +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/Lucense 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/src/test/scala/higherkindness/skeuomorph/instances.scala b/src/test/scala/higherkindness/skeuomorph/instances.scala index ec3e801f..0b1d2146 100644 --- a/src/test/scala/higherkindness/skeuomorph/instances.scala +++ b/src/test/scala/higherkindness/skeuomorph/instances.scala @@ -250,7 +250,7 @@ object instances { serversGen).mapN(Path.ItemObject.apply) val componentsGen: Gen[Components[T]] = - (responsesGen, mapStringToGen(eitherGen(requestGen, referenceGen))) + (mapStringToGen(T.arbitrary), responsesGen, mapStringToGen(eitherGen(requestGen, referenceGen))) .mapN(Components.apply) Arbitrary( diff --git a/src/test/scala/higherkindness/skeuomorph/openapi/OpenApiDecoderSpecification.scala b/src/test/scala/higherkindness/skeuomorph/openapi/OpenApiDecoderSpecification.scala index 88a2573d..1e1be190 100644 --- a/src/test/scala/higherkindness/skeuomorph/openapi/OpenApiDecoderSpecification.scala +++ b/src/test/scala/higherkindness/skeuomorph/openapi/OpenApiDecoderSpecification.scala @@ -154,6 +154,55 @@ class OpenApiDecoderSpecification extends org.specs2.mutable.Specification { val json = unsafeParse("{}") Decoder[Components[JsonSchemaF.Fixed]].decodeJson(json) must beRight( Components[JsonSchemaF.Fixed]( + Map.empty, + Map.empty, + Map.empty + ) + ) + } + "when an schemas are provided" >> { + val json = unsafeParse(""" + { + "schemas": { + "GeneralError": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + } + } + }, + "Category": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + } + } + } + } + """) + Decoder[Components[JsonSchemaF.Fixed]].decodeJson(json) must beRight( + Components[JsonSchemaF.Fixed]( + schemas = Map( + "GeneralError" -> Fixed.`object`( + List("code" -> Fixed.integer(), "message" -> Fixed.string()), + List.empty + ), + "Category" -> Fixed.`object`( + List("id" -> Fixed.long(), "name" -> Fixed.string()), + List.empty + ) + ), Map.empty, Map.empty ) diff --git a/src/test/scala/higherkindness/skeuomorph/openapi/OpenApiSchemaSpec.scala b/src/test/scala/higherkindness/skeuomorph/openapi/OpenApiSchemaSpec.scala index cec326f9..3030a37b 100644 --- a/src/test/scala/higherkindness/skeuomorph/openapi/OpenApiSchemaSpec.scala +++ b/src/test/scala/higherkindness/skeuomorph/openapi/OpenApiSchemaSpec.scala @@ -25,6 +25,7 @@ import schema._ import org.scalacheck._ import _root_.io.circe._ import _root_.io.circe.testing._ +import Spec._ class OpenApiSchemaSpec extends Specification with ScalaCheck with Discipline { @@ -47,32 +48,40 @@ class OpenApiSchemaSpec extends Specification with ScalaCheck with Discipline { CodecTests[OpenApi[JsonSchemaF.Fixed]].laws.codecRoundTrip(openApi) } - def shouldAbleToReadSpecExamples = Prop.forAll { (json: Json) => + def shouldAbleToReadSpecExamples = Prop.forAll { (format: Spec.Format) => import JsonDecoders._ + import yaml.{Decoder => _, _} + format.fold( + yaml.Decoder[OpenApi[JsonSchemaF.Fixed]].apply(_).isRight, + Decoder[OpenApi[JsonSchemaF.Fixed]].decodeJson(_).isRight + ) + } +} - Decoder[OpenApi[JsonSchemaF.Fixed]].decodeJson(json).isRight +object Spec { + type Yaml = String + type Format = Either[String, Json] - } + private val examples = + List("api-with-examples", "callback-example", "link-example", "petstore-expanded", "petstore", "uspto") - implicit val jsonArbitrary: Arbitrary[Json] = { + private def fromResource(fileName: String): String = + scala.io.Source + .fromInputStream(getClass.getResourceAsStream(fileName)) + .getLines() + .toList + .mkString("\n") - def readSpec(fileName: String): Json = - OpenApiDecoderSpecification.unsafeParse( - scala.io.Source - .fromInputStream(getClass.getResourceAsStream(s"/$fileName")) - .getLines() - .toList - .mkString("\n")) + private def examplesArbitrary[A](f: String => String)(g: String => A): Gen[A] = + Gen.oneOf( + examples + .map(x => g(fromResource(f(x)))) + ) + implicit val yamlArbitrary: Arbitrary[Format] = Arbitrary( - Gen.oneOf( - List( - "api-with-examples.json", - "callback-example.json", - "link-example.json", - "petstore-expanded.json", - "petstore.json", - "uspto.json").map(readSpec))) - } + eitherGen( + examplesArbitrary(x => s"/openapi/yaml/$x.yaml")(identity), + examplesArbitrary(x => s"/openapi/json/$x.json")(OpenApiDecoderSpecification.unsafeParse))) }