From a68159039068f848b8ea789a806c7b251d3db209 Mon Sep 17 00:00:00 2001 From: adamw Date: Mon, 9 Oct 2023 15:16:31 +0200 Subject: [PATCH 1/5] Update dependencies --- build.sbt | 6 +++--- project/plugins.sbt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.sbt b/build.sbt index 0f04c6d..862c6c5 100644 --- a/build.sbt +++ b/build.sbt @@ -4,14 +4,14 @@ 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) val scalaNativeVersions = List(scala2_12, scala2_13, scala3) -val circeVersion = "0.14.3" +val circeVersion = "0.14.6" val circeYamlVersion = "0.14.2" val scalaTestVersion = "3.2.17" diff --git a/project/plugins.sbt b/project/plugins.sbt index b715011..8036b89 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.14.0") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.14") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.15") addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.9.1") val sbtSoftwareMillVersion = "2.0.17" From 94b67a5712766a465ebe20b3819fc3c5bc2a57a9 Mon Sep 17 00:00:00 2001 From: adamw Date: Mon, 9 Oct 2023 15:20:45 +0200 Subject: [PATCH 2/5] Schemas can contain references, instead of being a reference or a schema --- .../src/main/scala/sttp/apispec/Schema.scala | 37 +++++---- .../src/main/scala/sttp/apispec/model.scala | 6 -- .../src/main/scala/sttp/apispec/package.scala | 2 - .../sttp/apispec/asyncapi/circe/package.scala | 17 +++- .../apispec/asyncapi/circe/EncoderTest.scala | 2 +- .../sttp/apispec/asyncapi/AsyncAPI.scala | 19 +++-- .../scala/sttp/apispec/asyncapi/package.scala | 5 ++ .../internal/JsonSchemaCirceDecoders.scala | 13 ++- .../internal/JsonSchemaCirceEncoders.scala | 15 +--- .../InternalSttpOpenAPICirceDecoders.scala | 4 +- .../InternalSttpOpenAPICirceEncoders.scala | 13 +++ .../apispec/openapi/circe/DecoderTest.scala | 10 +-- .../circe/overridden/EncoderTest.scala | 10 +-- .../circe/{ => threeone}/EncoderTest.scala | 82 ++++++++----------- .../scala/sttp/apispec/openapi/OpenAPI.scala | 30 +++---- .../scala/sttp/apispec/openapi/package.scala | 5 ++ 16 files changed, 140 insertions(+), 130 deletions(-) create mode 100644 asyncapi-model/src/main/scala/sttp/apispec/asyncapi/package.scala rename openapi-circe/src/test/scala/sttp/apispec/openapi/circe/{ => threeone}/EncoderTest.scala (65%) create mode 100644 openapi-model/src/main/scala/sttp/apispec/openapi/package.scala diff --git a/apispec-model/src/main/scala/sttp/apispec/Schema.scala b/apispec-model/src/main/scala/sttp/apispec/Schema.scala index cc7d885..8e8a014 100644 --- a/apispec-model/src/main/scala/sttp/apispec/Schema.scala +++ b/apispec-model/src/main/scala/sttp/apispec/Schema.scala @@ -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, @@ -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, @@ -57,18 +58,18 @@ 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]]) @@ -76,8 +77,10 @@ case class Discriminator(propertyName: String, mapping: Option[ListMap[String, S 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 diff --git a/apispec-model/src/main/scala/sttp/apispec/model.scala b/apispec-model/src/main/scala/sttp/apispec/model.scala index ddd7b24..5044de7 100644 --- a/apispec-model/src/main/scala/sttp/apispec/model.scala +++ b/apispec-model/src/main/scala/sttp/apispec/model.scala @@ -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 diff --git a/apispec-model/src/main/scala/sttp/apispec/package.scala b/apispec-model/src/main/scala/sttp/apispec/package.scala index e039ff5..5f843d1 100644 --- a/apispec-model/src/main/scala/sttp/apispec/package.scala +++ b/apispec-model/src/main/scala/sttp/apispec/package.scala @@ -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]] diff --git a/asyncapi-circe/src/main/scala/sttp/apispec/asyncapi/circe/package.scala b/asyncapi-circe/src/main/scala/sttp/apispec/asyncapi/circe/package.scala index 57d41fd..1268ae7 100644 --- a/asyncapi-circe/src/main/scala/sttp/apispec/asyncapi/circe/package.scala +++ b/asyncapi-circe/src/main/scala/sttp/apispec/asyncapi/circe/package.scala @@ -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. @@ -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] = diff --git a/asyncapi-circe/src/test/scala/sttp/apispec/asyncapi/circe/EncoderTest.scala b/asyncapi-circe/src/test/scala/sttp/apispec/asyncapi/circe/EncoderTest.scala index 02ea7b1..57c12cf 100644 --- a/asyncapi-circe/src/test/scala/sttp/apispec/asyncapi/circe/EncoderTest.scala +++ b/asyncapi-circe/src/test/scala/sttp/apispec/asyncapi/circe/EncoderTest.scala @@ -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")) ) ) ) diff --git a/asyncapi-model/src/main/scala/sttp/apispec/asyncapi/AsyncAPI.scala b/asyncapi-model/src/main/scala/sttp/apispec/asyncapi/AsyncAPI.scala index dbe7821..01537fd 100644 --- a/asyncapi-model/src/main/scala/sttp/apispec/asyncapi/AsyncAPI.scala +++ b/asyncapi-model/src/main/scala/sttp/apispec/asyncapi/AsyncAPI.scala @@ -4,7 +4,6 @@ import sttp.apispec.{ ExampleValue, ExtensionValue, ExternalDocumentation, - ReferenceOr, Schema, SecurityRequirement, SecurityScheme, @@ -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, @@ -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, @@ -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, @@ -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}") +} diff --git a/asyncapi-model/src/main/scala/sttp/apispec/asyncapi/package.scala b/asyncapi-model/src/main/scala/sttp/apispec/asyncapi/package.scala new file mode 100644 index 0000000..e975198 --- /dev/null +++ b/asyncapi-model/src/main/scala/sttp/apispec/asyncapi/package.scala @@ -0,0 +1,5 @@ +package sttp.apispec + +package object asyncapi { + type ReferenceOr[T] = Either[Reference, T] +} diff --git a/jsonschema-circe/src/main/scala/sttp/apispec/internal/JsonSchemaCirceDecoders.scala b/jsonschema-circe/src/main/scala/sttp/apispec/internal/JsonSchemaCirceDecoders.scala index 44cc536..a7fe70d 100644 --- a/jsonschema-circe/src/main/scala/sttp/apispec/internal/JsonSchemaCirceDecoders.scala +++ b/jsonschema-circe/src/main/scala/sttp/apispec/internal/JsonSchemaCirceDecoders.scala @@ -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 @@ -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]] = diff --git a/jsonschema-circe/src/main/scala/sttp/apispec/internal/JsonSchemaCirceEncoders.scala b/jsonschema-circe/src/main/scala/sttp/apispec/internal/JsonSchemaCirceEncoders.scala index 45c9971..9945353 100644 --- a/jsonschema-circe/src/main/scala/sttp/apispec/internal/JsonSchemaCirceEncoders.scala +++ b/jsonschema-circe/src/main/scala/sttp/apispec/internal/JsonSchemaCirceEncoders.scala @@ -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, @@ -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))) @@ -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)): _*) diff --git a/openapi-circe/src/main/scala/sttp/apispec/openapi/internal/InternalSttpOpenAPICirceDecoders.scala b/openapi-circe/src/main/scala/sttp/apispec/openapi/internal/InternalSttpOpenAPICirceDecoders.scala index 3c02ddd..0e307fb 100644 --- a/openapi-circe/src/main/scala/sttp/apispec/openapi/internal/InternalSttpOpenAPICirceDecoders.scala +++ b/openapi-circe/src/main/scala/sttp/apispec/openapi/internal/InternalSttpOpenAPICirceDecoders.scala @@ -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] @@ -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") diff --git a/openapi-circe/src/main/scala/sttp/apispec/openapi/internal/InternalSttpOpenAPICirceEncoders.scala b/openapi-circe/src/main/scala/sttp/apispec/openapi/internal/InternalSttpOpenAPICirceEncoders.scala index 586bb50..1b15507 100644 --- a/openapi-circe/src/main/scala/sttp/apispec/openapi/internal/InternalSttpOpenAPICirceEncoders.scala +++ b/openapi-circe/src/main/scala/sttp/apispec/openapi/internal/InternalSttpOpenAPICirceEncoders.scala @@ -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) diff --git a/openapi-circe/src/test/scala/sttp/apispec/openapi/circe/DecoderTest.scala b/openapi-circe/src/test/scala/sttp/apispec/openapi/circe/DecoderTest.scala index 4dd1fd1..afbd532 100644 --- a/openapi-circe/src/test/scala/sttp/apispec/openapi/circe/DecoderTest.scala +++ b/openapi-circe/src/test/scala/sttp/apispec/openapi/circe/DecoderTest.scala @@ -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") { @@ -31,8 +31,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_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") { @@ -40,7 +40,7 @@ class DecoderTest extends AnyFunSuite with ResourcePlatform { 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) } diff --git a/openapi-circe/src/test/scala/sttp/apispec/openapi/circe/overridden/EncoderTest.scala b/openapi-circe/src/test/scala/sttp/apispec/openapi/circe/overridden/EncoderTest.scala index b1ea893..2112ca0 100644 --- a/openapi-circe/src/test/scala/sttp/apispec/openapi/circe/overridden/EncoderTest.scala +++ b/openapi-circe/src/test/scala/sttp/apispec/openapi/circe/overridden/EncoderTest.scala @@ -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) ) @@ -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) ) diff --git a/openapi-circe/src/test/scala/sttp/apispec/openapi/circe/EncoderTest.scala b/openapi-circe/src/test/scala/sttp/apispec/openapi/circe/threeone/EncoderTest.scala similarity index 65% rename from openapi-circe/src/test/scala/sttp/apispec/openapi/circe/EncoderTest.scala rename to openapi-circe/src/test/scala/sttp/apispec/openapi/circe/threeone/EncoderTest.scala index 556cb55..1f6978f 100644 --- a/openapi-circe/src/test/scala/sttp/apispec/openapi/circe/EncoderTest.scala +++ b/openapi-circe/src/test/scala/sttp/apispec/openapi/circe/threeone/EncoderTest.scala @@ -1,10 +1,10 @@ -package sttp.apispec -package openapi -package circe -package threeone +package sttp.apispec.openapi.circe.threeone import io.circe.syntax._ import org.scalatest.funsuite.AnyFunSuite +import sttp.apispec._ +import sttp.apispec.openapi._ +import sttp.apispec.openapi.circe.SttpOpenAPICirceEncoders import sttp.apispec.test._ import scala.collection.immutable.ListMap @@ -63,48 +63,38 @@ class EncoderTest extends AnyFunSuite with ResourcePlatform with SttpOpenAPICirc val components = Components( schemas = ListMap( - "model" -> refOr( - Schema(SchemaType.Object).copy( - properties = ListMap( - "one" -> refOr( - schemaTypeAndDescription("type array", ArraySchemaType(List(SchemaType.Integer, SchemaType.String))) - ), - "two" -> refOr(schemaTypeAndDescription("type 'null'", SchemaType.Null)), - "three" -> refOr( - schemaTypeAndDescription( - "type array including 'null'", - ArraySchemaType(List(SchemaType.String, SchemaType.Null)) - ) - ), - "four" -> refOr(schemaTypeAndDescription("array with no items", SchemaType.Array)), - "five" -> refOr( - schemaTypeAndDescription("singular example", SchemaType.String) - .copy(example = Some(ExampleSingleValue("exampleValue"))) - ), - "six" -> refOr( - Schema( - description = Some("exclusiveMinimum true"), - exclusiveMinimum = Some(true), - minimum = Some(BigDecimal(10)) - ) - ), - "seven" -> refOr(Schema(description = Some("exclusiveMinimum false"), minimum = Some(BigDecimal(10)))), - "eight" -> refOr( - Schema( - description = Some("exclusiveMaximum true"), - exclusiveMaximum = Some(true), - maximum = Some(BigDecimal(20)) - ) - ), - "nine" -> refOr(Schema(description = Some("exclusiveMaximum false"), maximum = Some(BigDecimal(20)))), - "ten" -> refOr( - schemaTypeAndDescription("nullable string", SchemaType.String).copy(nullable = Some(true)) - ), - "eleven" -> refOr( - schemaTypeAndDescription("x-nullable string", ArraySchemaType(List(SchemaType.String, SchemaType.Null))) - ), - "twelve" -> refOr(Schema(description = Some("file/binary"))) - ) + "model" -> Schema(SchemaType.Object).copy( + properties = ListMap( + "one" -> schemaTypeAndDescription( + "type array", + ArraySchemaType(List(SchemaType.Integer, SchemaType.String)) + ), + "two" -> schemaTypeAndDescription("type 'null'", SchemaType.Null), + "three" -> schemaTypeAndDescription( + "type array including 'null'", + ArraySchemaType(List(SchemaType.String, SchemaType.Null)) + ), + "four" -> schemaTypeAndDescription("array with no items", SchemaType.Array), + "five" -> schemaTypeAndDescription("singular example", SchemaType.String) + .copy(example = Some(ExampleSingleValue("exampleValue"))), + "six" -> Schema( + description = Some("exclusiveMinimum true"), + exclusiveMinimum = Some(true), + minimum = Some(BigDecimal(10)) + ), + "seven" -> Schema(description = Some("exclusiveMinimum false"), minimum = Some(BigDecimal(10))), + "eight" -> Schema( + description = Some("exclusiveMaximum true"), + exclusiveMaximum = Some(true), + maximum = Some(BigDecimal(20)) + ), + "nine" -> Schema(description = Some("exclusiveMaximum false"), maximum = Some(BigDecimal(20))), + "ten" -> schemaTypeAndDescription("nullable string", SchemaType.String).copy(nullable = Some(true)), + "eleven" -> schemaTypeAndDescription( + "x-nullable string", + ArraySchemaType(List(SchemaType.String, SchemaType.Null)) + ), + "twelve" -> Schema(description = Some("file/binary")) ) ) ) diff --git a/openapi-model/src/main/scala/sttp/apispec/openapi/OpenAPI.scala b/openapi-model/src/main/scala/sttp/apispec/openapi/OpenAPI.scala index 103c586..d99de83 100644 --- a/openapi-model/src/main/scala/sttp/apispec/openapi/OpenAPI.scala +++ b/openapi-model/src/main/scala/sttp/apispec/openapi/OpenAPI.scala @@ -4,8 +4,6 @@ import sttp.apispec.{ ExampleValue, ExtensionValue, ExternalDocumentation, - Reference, - ReferenceOr, SchemaLike, SecurityRequirement, SecurityScheme, @@ -144,7 +142,7 @@ final case class ServerVariable( } final case class Components( - schemas: ListMap[String, ReferenceOr[SchemaLike]] = ListMap.empty, + schemas: ListMap[String, SchemaLike] = ListMap.empty, responses: ListMap[String, ReferenceOr[Response]] = ListMap.empty, parameters: ListMap[String, ReferenceOr[Parameter]] = ListMap.empty, examples: ListMap[String, ReferenceOr[Example]] = ListMap.empty, @@ -155,16 +153,14 @@ final case class Components( callbacks: ListMap[String, ReferenceOr[Callback]] = ListMap.empty, extensions: ListMap[String, ExtensionValue] = ListMap.empty ) { - def addSchema(key: String, schema: SchemaLike): Components = copy(schemas = schemas.updated(key, Right(schema))) - def getLocalSchema(key: String): Option[SchemaLike] = schemas.get(key).flatMap(_.toOption) - def getReferenceToSchema(key: String): Option[Reference] = - schemas.get(key).map(refOr => refOr.fold(identity, _ => Reference(s"#/components/schemas/$key"))) + def addSchema(key: String, schema: SchemaLike): Components = copy(schemas = schemas.updated(key, schema)) + def getSchema(key: String): Option[SchemaLike] = schemas.get(key) def addSecurityScheme(key: String, scheme: SecurityScheme): Components = copy(securitySchemes = securitySchemes.updated(key, Right(scheme))) def getLocalSecurityScheme(key: String): Option[SecurityScheme] = securitySchemes.get(key).flatMap(_.toOption) def getReferenceToSecurityScheme(key: String): Option[Reference] = securitySchemes.get(key).map(refOr => refOr.fold(identity, _ => Reference(s"#/components/securitySchemes/$key"))) - def schemas(updated: ListMap[String, ReferenceOr[SchemaLike]]): Components = copy(schemas = updated) + def schemas(updated: ListMap[String, SchemaLike]): Components = copy(schemas = updated) def securitySchemes(updated: ListMap[String, ReferenceOr[SecurityScheme]]): Components = copy(securitySchemes = updated) def addResponse(key: String, response: Response): Components = @@ -344,7 +340,7 @@ final case class Parameter( style: Option[ParameterStyle] = None, explode: Option[Boolean] = None, allowReserved: Option[Boolean] = None, - schema: Option[ReferenceOr[SchemaLike]], + schema: Option[SchemaLike], example: Option[ExampleValue] = None, examples: ListMap[String, ReferenceOr[Example]] = ListMap.empty, content: ListMap[String, MediaType] = ListMap.empty, @@ -359,7 +355,7 @@ final case class Parameter( def style(updated: ParameterStyle): Parameter = copy(style = Some(updated)) def explode(updated: Boolean): Parameter = copy(explode = Some(updated)) def allowReserved(updated: Boolean): Parameter = copy(allowReserved = Some(updated)) - def schema(updated: SchemaLike): Parameter = copy(schema = Some(Right(updated))) + def schema(updated: SchemaLike): Parameter = copy(schema = Some(updated)) def example(updated: ExampleValue): Parameter = copy(example = Some(updated)) def examples(updated: ListMap[String, ReferenceOr[Example]]): Parameter = copy(examples = updated) def addExample(key: String, updated: Example): Parameter = copy(examples = examples.updated(key, Right(updated))) @@ -406,13 +402,13 @@ object RequestBody { } final case class MediaType( - schema: Option[ReferenceOr[SchemaLike]] = None, + schema: Option[SchemaLike] = None, example: Option[ExampleValue] = None, examples: ListMap[String, ReferenceOr[Example]] = ListMap.empty, encoding: ListMap[String, Encoding] = ListMap.empty, extensions: ListMap[String, ExtensionValue] = ListMap.empty ) { - def schema(updated: SchemaLike): MediaType = copy(schema = Some(Right(updated))) + def schema(updated: SchemaLike): MediaType = copy(schema = Some(updated)) def example(updated: ExampleValue): MediaType = copy(example = Some(updated)) def examples(updated: ListMap[String, ReferenceOr[Example]]): MediaType = copy(examples = updated) def addExample(key: String, updated: Example): MediaType = copy(examples = examples.updated(key, Right(updated))) @@ -518,7 +514,7 @@ final case class Header( style: Option[ParameterStyle] = None, explode: Option[Boolean] = None, allowReserved: Option[Boolean] = None, - schema: Option[ReferenceOr[SchemaLike]] = None, + schema: Option[SchemaLike] = None, example: Option[ExampleValue] = None, examples: ListMap[String, ReferenceOr[Example]] = ListMap.empty, content: ListMap[String, MediaType] = ListMap.empty @@ -530,7 +526,7 @@ final case class Header( def style(updated: ParameterStyle): Header = copy(style = Some(updated)) def explode(updated: Boolean): Header = copy(explode = Some(updated)) def allowReserved(updated: Boolean): Header = copy(allowReserved = Some(updated)) - def schema(updated: SchemaLike): Header = copy(schema = Some(Right(updated))) + def schema(updated: SchemaLike): Header = copy(schema = Some(updated)) def example(updated: ExampleValue): Header = copy(example = Some(updated)) def examples(updated: ListMap[String, ReferenceOr[Example]]): Header = copy(examples = updated) def addExample(key: String, updated: Example): Header = copy(examples = examples.updated(key, Right(updated))) @@ -573,3 +569,9 @@ final case class Callback(pathItems: ListMap[String, ReferenceOr[PathItem]] = Li object Callback { val Empty: Callback = Callback() } + +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}") +} diff --git a/openapi-model/src/main/scala/sttp/apispec/openapi/package.scala b/openapi-model/src/main/scala/sttp/apispec/openapi/package.scala new file mode 100644 index 0000000..0da28dd --- /dev/null +++ b/openapi-model/src/main/scala/sttp/apispec/openapi/package.scala @@ -0,0 +1,5 @@ +package sttp.apispec + +package object openapi { + type ReferenceOr[T] = Either[Reference, T] +} From 4c37084c35d51f27af1dfaa85d3405514d282baa Mon Sep 17 00:00:00 2001 From: adamw Date: Mon, 9 Oct 2023 15:56:37 +0200 Subject: [PATCH 3/5] Update runner --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 659c877..ac6bafe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 From cce55f430c5011b4ae2972575c8db202899a3812 Mon Sep 17 00:00:00 2001 From: adamw Date: Mon, 9 Oct 2023 15:58:43 +0200 Subject: [PATCH 4/5] Update runner everywhere --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac6bafe..6134829 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: @@ -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 From 94b33996452f5d1608e8b3e28042a03f43f205a9 Mon Sep 17 00:00:00 2001 From: adamw Date: Mon, 9 Oct 2023 16:08:41 +0200 Subject: [PATCH 5/5] Downgrade circe --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 862c6c5..948c001 100644 --- a/build.sbt +++ b/build.sbt @@ -11,7 +11,7 @@ val scalaJVMVersions = List(scala2_12, scala2_13, scala3) val scalaJSVersions = List(scala2_12, scala2_13, scala3) val scalaNativeVersions = List(scala2_12, scala2_13, scala3) -val circeVersion = "0.14.6" +val circeVersion = "0.14.3" val circeYamlVersion = "0.14.2" val scalaTestVersion = "3.2.17"