Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ff/open api to mu #90

Merged
merged 3 commits into from
May 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
13 changes: 7 additions & 6 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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] =
Expand All @@ -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")
Expand All @@ -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
}

Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]]
)
Expand Down
32 changes: 32 additions & 0 deletions src/main/scala/higherkindness/skeuomorph/openapi/yaml.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2018-2019 47 Degrees, LLC. <http://www.47deg.com>
*
* 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))

}
167 changes: 167 additions & 0 deletions src/test/resources/openapi/yaml/api-with-examples.yaml
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
}
60 changes: 60 additions & 0 deletions src/test/resources/openapi/yaml/callback-example.yaml
Original file line number Diff line number Diff line change
@@ -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