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

Schemas contain references to other schemas, instead of being either referenced to, or a "proper" schema #117

Merged
merged 5 commits into from
Oct 9, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
ci:
# run on external PRs, but not on internal PRs since those will be run by push to branch
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
env:
STTP_NATIVE: 1
JAVA_OPTS: -Xmx4G
Expand Down Expand Up @@ -47,7 +47,7 @@ jobs:
mima:
# run on external PRs, but not on internal PRs since those will be run by push to branch
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
env:
JAVA_OPTS: -Xmx4G
steps:
Expand Down Expand Up @@ -81,7 +81,7 @@ jobs:
name: Publish release
needs: [ci]
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v'))
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
env:
STTP_NATIVE: 1
JAVA_OPTS: -Xmx4G
Expand Down
37 changes: 20 additions & 17 deletions apispec-model/src/main/scala/sttp/apispec/Schema.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,17 @@ object AnySchema {

// todo: xml
case class Schema(
$ref: Option[String] = None,
$schema: Option[String] = None,
allOf: List[ReferenceOr[SchemaLike]] = List.empty,
allOf: List[SchemaLike] = List.empty,
title: Option[String] = None,
required: List[String] = List.empty,
`type`: Option[SchemaType] = None,
prefixItems: Option[List[ReferenceOr[SchemaLike]]] = None,
items: Option[ReferenceOr[SchemaLike]] = None,
contains: Option[ReferenceOr[SchemaLike]] = None,
properties: ListMap[String, ReferenceOr[SchemaLike]] = ListMap.empty,
patternProperties: ListMap[Pattern, ReferenceOr[SchemaLike]] = ListMap.empty,
prefixItems: Option[List[SchemaLike]] = None,
items: Option[SchemaLike] = None,
contains: Option[SchemaLike] = None,
properties: ListMap[String, SchemaLike] = ListMap.empty,
patternProperties: ListMap[Pattern, SchemaLike] = ListMap.empty,
description: Option[String] = None,
format: Option[String] = None,
default: Option[ExampleValue] = None,
Expand All @@ -44,9 +45,9 @@ case class Schema(
writeOnly: Option[Boolean] = None,
example: Option[ExampleValue] = None,
deprecated: Option[Boolean] = None,
oneOf: List[ReferenceOr[SchemaLike]] = List.empty,
oneOf: List[SchemaLike] = List.empty,
discriminator: Option[Discriminator] = None,
additionalProperties: Option[ReferenceOr[SchemaLike]] = None,
additionalProperties: Option[SchemaLike] = None,
pattern: Option[Pattern] = None,
minLength: Option[Int] = None,
maxLength: Option[Int] = None,
Expand All @@ -57,27 +58,29 @@ case class Schema(
minItems: Option[Int] = None,
maxItems: Option[Int] = None,
`enum`: Option[List[ExampleSingleValue]] = None,
not: Option[ReferenceOr[SchemaLike]] = None,
`if`: Option[ReferenceOr[SchemaLike]] = None,
`then`: Option[ReferenceOr[SchemaLike]] = None,
`else`: Option[ReferenceOr[SchemaLike]] = None,
not: Option[SchemaLike] = None,
`if`: Option[SchemaLike] = None,
`then`: Option[SchemaLike] = None,
`else`: Option[SchemaLike] = None,
$defs: Option[ListMap[String, SchemaLike]] = None,
extensions: ListMap[String, ExtensionValue] = ListMap.empty,
$id: Option[String] = None,
const: Option[ExampleValue] = None,
anyOf: List[ReferenceOr[SchemaLike]] = List.empty,
unevaluatedProperties: Option[ReferenceOr[SchemaLike]] = None,
anyOf: List[SchemaLike] = List.empty,
unevaluatedProperties: Option[SchemaLike] = None,
dependentRequired: ListMap[String, List[String]] = ListMap.empty,
dependentSchemas: ListMap[String, ReferenceOr[SchemaLike]] = ListMap.empty
dependentSchemas: ListMap[String, SchemaLike] = ListMap.empty
) extends SchemaLike

case class Discriminator(propertyName: String, mapping: Option[ListMap[String, String]])

object Schema {
def apply(schemaType: SchemaType): Schema = new Schema(`type` = Some(schemaType))

def apply(references: List[ReferenceOr[Schema]], discriminator: Option[Discriminator]): Schema =
new Schema(oneOf = references, discriminator = discriminator)
def oneOf(references: List[SchemaLike], discriminator: Option[Discriminator]): Schema =
Schema(oneOf = references, discriminator = discriminator)

def referenceTo(prefix: String, $ref: String): Schema = Schema($ref = Some(s"$prefix${$ref}"))
}

sealed trait SchemaType
Expand Down
6 changes: 0 additions & 6 deletions apispec-model/src/main/scala/sttp/apispec/model.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,6 @@ package sttp.apispec

import scala.collection.immutable.ListMap

case class Reference($ref: String, summary: Option[String] = None, description: Option[String] = None)

object Reference {
def to(prefix: String, $ref: String): Reference = new Reference(s"$prefix${$ref}")
}

sealed trait ExampleValue
case class ExampleSingleValue(value: Any) extends ExampleValue
case class ExampleMultipleValue(values: List[Any]) extends ExampleValue
Expand Down
2 changes: 0 additions & 2 deletions apispec-model/src/main/scala/sttp/apispec/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package sttp
import scala.collection.immutable.ListMap

package object apispec {
type ReferenceOr[T] = Either[Reference, T]

// using a Vector instead of a List, as empty Lists are always encoded as nulls
// here, we need them encoded as an empty array
type SecurityRequirement = ListMap[String, Vector[String]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,18 @@ package circe {

trait SttpAsyncAPICirceEncoders extends JsonSchemaCirceEncoders {
// note: these are strict val-s, order matters!
override val openApi30: Boolean = true
implicit val encoderReference: Encoder[Reference] = deriveEncoder[Reference]
implicit def encoderReferenceOr[T: Encoder]: Encoder[ReferenceOr[T]] = {
case Left(Reference(ref, summary, description)) =>
Json
.obj(
s"$$ref" := ref,
"summary" := summary,
"description" := description
)
.dropNullValues
case Right(t) => implicitly[Encoder[T]].apply(t)
}

implicit val encoderOAuthFlow: Encoder[OAuthFlow] = {
// #79: all OAuth flow object MUST include a scopes field, but it MAY be empty.
Expand Down Expand Up @@ -111,10 +122,10 @@ package circe {

private def nullIfEmpty[T](a: List[T])(otherwise: => Json): Json = if (a.isEmpty) Json.Null else otherwise

implicit val encoderMessagePayload: Encoder[Option[Either[AnyValue, ReferenceOr[Schema]]]] = {
implicit val encoderMessagePayload: Encoder[Option[Either[AnyValue, Schema]]] = {
case None => Json.Null
case Some(Left(av)) => encoderAnyValue.apply(av)
case Some(Right(s)) => encoderReferenceOr[Schema].apply(s)
case Some(Right(s)) => encoderSchema.apply(s)
}

implicit val encoderMessageTrait: Encoder[MessageTrait] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class EncoderTest extends AnyFunSuite {
val comp = Components(messages =
ListMap(
"string" -> Right(
SingleMessage(payload = Some(Right(Right(Schema(SchemaType.String)))), contentType = Some("text/plain"))
SingleMessage(payload = Some(Right(Schema(SchemaType.String))), contentType = Some("text/plain"))
)
)
)
Expand Down
19 changes: 12 additions & 7 deletions asyncapi-model/src/main/scala/sttp/apispec/asyncapi/AsyncAPI.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import sttp.apispec.{
ExampleValue,
ExtensionValue,
ExternalDocumentation,
ReferenceOr,
Schema,
SecurityRequirement,
SecurityScheme,
Expand Down Expand Up @@ -166,9 +165,9 @@ object Message {
}
case class OneOfMessage(oneOf: List[SingleMessage]) extends Message
case class SingleMessage(
headers: Option[ReferenceOr[Schema]] = None,
payload: Option[Either[AnyValue, ReferenceOr[Schema]]] = None,
correlationId: Option[ReferenceOr[Schema]] = None,
headers: Option[Schema] = None,
payload: Option[Either[AnyValue, Schema]] = None,
correlationId: Option[Schema] = None,
schemaFormat: Option[String] = None,
contentType: Option[String] = None,
name: Option[String] = None,
Expand All @@ -184,8 +183,8 @@ case class SingleMessage(
) extends Message

case class MessageTrait(
headers: Option[ReferenceOr[Schema]] = None,
correlationId: Option[ReferenceOr[Schema]] = None,
headers: Option[Schema] = None,
correlationId: Option[Schema] = None,
schemaFormat: Option[String] = None,
contentType: Option[String] = None,
name: Option[String] = None,
Expand All @@ -201,7 +200,7 @@ case class MessageTrait(

// TODO: serverBindings, channelBindings, operationBindings, messageBindings
case class Components(
schemas: ListMap[String, ReferenceOr[Schema]] = ListMap.empty,
schemas: ListMap[String, Schema] = ListMap.empty,
messages: ListMap[String, ReferenceOr[Message]] = ListMap.empty,
securitySchemes: ListMap[String, ReferenceOr[SecurityScheme]] = ListMap.empty,
parameters: ListMap[String, ReferenceOr[Parameter]] = ListMap.empty,
Expand All @@ -218,3 +217,9 @@ case class CorrelationId(
)

case class AnyValue(value: String)

case class Reference($ref: String, summary: Option[String] = None, description: Option[String] = None)

object Reference {
def to(prefix: String, $ref: String): Reference = new Reference(s"$prefix${$ref}")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package sttp.apispec

package object asyncapi {
type ReferenceOr[T] = Either[Reference, T]
}
4 changes: 2 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import sbt.Reference.display
import sbt.internal.ProjectMatrix

val scala2_12 = "2.12.18"
val scala2_13 = "2.13.11"
val scala3 = "3.3.0"
val scala2_13 = "2.13.12"
val scala3 = "3.3.1"

val scalaJVMVersions = List(scala2_12, scala2_13, scala3)
val scalaJSVersions = List(scala2_12, scala2_13, scala3)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ import io.circe.generic.semiauto.deriveDecoder
import scala.collection.immutable.ListMap

trait JsonSchemaCirceDecoders {
implicit val referenceDecoder: Decoder[Reference] = deriveDecoder[Reference]
implicit def decodeReferenceOr[A: Decoder]: Decoder[ReferenceOr[A]] = referenceDecoder.either(Decoder[A])

implicit val decodeBasicSchemaType: Decoder[BasicSchemaType] = Decoder.decodeString.emap {
case SchemaType.Integer.value => SchemaType.Integer.asRight
case SchemaType.Boolean.value => SchemaType.Boolean.asRight
Expand Down Expand Up @@ -57,13 +54,13 @@ trait JsonSchemaCirceDecoders {
Decoder.decodeMapLike[String, ExtensionValue, ListMap].map(_.filter(_._1.startsWith("x-")))

implicit val schemaDecoder: Decoder[Schema] = {
implicit def listMapDecoder[A: Decoder]: Decoder[ListMap[String, ReferenceOr[A]]] =
Decoder.decodeOption(Decoder.decodeMapLike[String, ReferenceOr[A], ListMap]).map(_.getOrElse(ListMap.empty))
implicit def listMapDecoder[A: Decoder]: Decoder[ListMap[String, A]] =
Decoder.decodeOption(Decoder.decodeMapLike[String, A, ListMap]).map(_.getOrElse(ListMap.empty))

implicit def listPatternMapDecoder[A: Decoder]: Decoder[ListMap[Pattern, ReferenceOr[A]]] =
Decoder.decodeOption(Decoder.decodeMapLike[Pattern, ReferenceOr[A], ListMap]).map(_.getOrElse(ListMap.empty))
implicit def listPatternMapDecoder[A: Decoder]: Decoder[ListMap[Pattern, A]] =
Decoder.decodeOption(Decoder.decodeMapLike[Pattern, A, ListMap]).map(_.getOrElse(ListMap.empty))

implicit def listdependentFieldsDecoder: Decoder[ListMap[String, List[String]]] =
implicit def listDependentFieldsDecoder: Decoder[ListMap[String, List[String]]] =
Decoder.decodeOption(Decoder.decodeMapLike[String, List[String], ListMap]).map(_.getOrElse(ListMap.empty))

implicit def listReference[A: Decoder]: Decoder[List[A]] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ trait JsonSchemaCirceEncoders {
val maxKey = if (s.exclusiveMaximum.getOrElse(false)) "exclusiveMaximum" else "maximum"
JsonObject(
s"$$id" := s.$id,
s"$$ref" := s.$ref,
s"$$schema" := s.$schema,
"allOf" := s.allOf,
"anyOf" := s.anyOf,
Expand Down Expand Up @@ -105,18 +106,6 @@ trait JsonSchemaCirceEncoders {
.mapJsonObject(expandExtensions)

// note: these are strict val-s, order matters!
implicit def encoderReferenceOr[T: Encoder]: Encoder[ReferenceOr[T]] = {
case Left(Reference(ref, summary, description)) =>
Json
.obj(
s"$$ref" := ref,
"summary" := summary,
"description" := description
)
.dropNullValues
case Right(t) => implicitly[Encoder[T]].apply(t)
}

implicit val extensionValue: Encoder[ExtensionValue] =
Encoder.instance(e => parse(e.value).getOrElse(Json.fromString(e.value)))

Expand Down Expand Up @@ -176,8 +165,6 @@ trait JsonSchemaCirceEncoders {
case s: Schema => encoderSchema(s)
}

implicit val encoderReference: Encoder[Reference] = deriveEncoder[Reference]

implicit def encodeList[T: Encoder]: Encoder[List[T]] = {
case Nil => Json.Null
case l: List[T] => Json.arr(l.map(i => implicitly[Encoder[T]].apply(i)): _*)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import sttp.apispec.internal.JsonSchemaCirceDecoders
import scala.collection.immutable.ListMap

trait InternalSttpOpenAPICirceDecoders extends JsonSchemaCirceDecoders {
implicit val referenceDecoder: Decoder[Reference] = deriveDecoder[Reference]
implicit def decodeReferenceOr[A: Decoder]: Decoder[ReferenceOr[A]] = referenceDecoder.either(Decoder[A])

implicit val externalDocumentationDecoder: Decoder[ExternalDocumentation] = withExtensions(
deriveDecoder[ExternalDocumentation]
Expand Down Expand Up @@ -115,7 +117,7 @@ trait InternalSttpOpenAPICirceDecoders extends JsonSchemaCirceDecoders {
def getComp[A: Decoder](name: String): Decoder.Result[Comp[A]] =
c.get[Option[Comp[A]]](name).map(_.getOrElse(ListMap.empty))
for {
schemas <- getComp[SchemaLike]("schemas")
schemas <- c.get[Option[ListMap[String, SchemaLike]]]("schemas").map(_.getOrElse(ListMap.empty))
responses <- getComp[Response]("responses")
parameters <- getComp[Parameter]("parameters")
examples <- getComp[Example]("examples")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ import sttp.apispec.internal.JsonSchemaCirceEncoders
import scala.collection.immutable.ListMap

trait InternalSttpOpenAPICirceEncoders extends JsonSchemaCirceEncoders {
implicit val encoderReference: Encoder[Reference] = deriveEncoder[Reference]
implicit def encoderReferenceOr[T: Encoder]: Encoder[ReferenceOr[T]] = {
case Left(Reference(ref, summary, description)) =>
Json
.obj(
s"$$ref" := ref,
"summary" := summary,
"description" := description
)
.dropNullValues
case Right(t) => implicitly[Encoder[T]].apply(t)
}

implicit val encoderOAuthFlow: Encoder[OAuthFlow] = {
// #79: all OAuth flow object MUST include a scopes field, but it MAY be empty.
implicit def encodeListMap: Encoder[ListMap[String, String]] = doEncodeListMap(nullWhenEmpty = false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ class DecoderTest extends AnyFunSuite with ResourcePlatform {
assert(openapi.info.title === "API")
val schemas = openapi.components.getOrElse(Components.Empty).schemas
assert(schemas.nonEmpty)
assert(schemas("anything_boolean") === Right(AnySchema.Anything))
assert(schemas("nothing_boolean") === Right(AnySchema.Nothing))
assert(schemas("anything_boolean") === AnySchema.Anything)
assert(schemas("nothing_boolean") === AnySchema.Nothing)
}

test("spec any nothing schema object") {
Expand All @@ -31,16 +31,16 @@ class DecoderTest extends AnyFunSuite with ResourcePlatform {
assert(openapi.info.title === "API")
val schemas = openapi.components.getOrElse(Components.Empty).schemas
assert(schemas.nonEmpty)
assert(schemas("anything_object") === Right(AnySchema.Anything))
assert(schemas("nothing_object") === Right(AnySchema.Nothing))
assert(schemas("anything_object") === AnySchema.Anything)
assert(schemas("nothing_object") === AnySchema.Nothing)
}

test("all schemas types") {
val Right(openapi) = readJson("/spec/3.1/schema.json").flatMap(_.as[OpenAPI]): @unchecked
assert(openapi.info.title === "API")
val schemas = openapi.components.getOrElse(Components.Empty).schemas
assert(schemas.nonEmpty)
val Right(model) = schemas("model"): @unchecked
val model = schemas("model")
assert(model.asInstanceOf[Schema].properties.size === 12)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,12 @@ class EncoderTest extends AnyFunSuite with ResourcePlatform {

val components = Components(
schemas = ListMap(
"anything_boolean" -> refOr(AnySchema.Anything),
"nothing_boolean" -> refOr(AnySchema.Nothing)
"anything_boolean" -> AnySchema.Anything,
"nothing_boolean" -> AnySchema.Nothing
)
)

val openapi = OpenAPI(
openapi = "3.1.0",
info = Info(title = "API", version = "1.0.0"),
components = Some(components)
)
Expand All @@ -43,13 +42,12 @@ class EncoderTest extends AnyFunSuite with ResourcePlatform {

val components = Components(
schemas = ListMap(
"anything_object" -> refOr(AnySchema.Anything),
"nothing_object" -> refOr(AnySchema.Nothing)
"anything_object" -> AnySchema.Anything,
"nothing_object" -> AnySchema.Nothing
)
)

val openapi = OpenAPI(
openapi = "3.1.0",
info = Info(title = "API", version = "1.0.0"),
components = Some(components)
)
Expand Down
Loading