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

Add JsonSchema fields #69

Merged
merged 3 commits into from
May 30, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
8 changes: 7 additions & 1 deletion apispec-model/src/main/scala/sttp/apispec/Schema.scala
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,13 @@ case class Schema(
`then`: Option[ReferenceOr[SchemaLike]] = None,
`else`: Option[ReferenceOr[SchemaLike]] = None,
$defs: Option[ListMap[String, SchemaLike]] = None,
extensions: ListMap[String, ExtensionValue] = ListMap.empty
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,
dependentRequired: ListMap[String, List[String]] = ListMap.empty,
dependentSchemas: ListMap[String, ReferenceOr[SchemaLike]] = ListMap.empty
) extends SchemaLike

case class Discriminator(propertyName: String, mapping: Option[ListMap[String, String]])
Expand Down
29 changes: 26 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ val commonNativeSettings = commonSettings ++ Seq(
)

lazy val allProjectAggregates: Seq[ProjectReference] =
apispecModel.projectRefs ++
circeTestUtils.projectRefs ++
apispecModel.projectRefs ++
jsonSchemaCirce.projectRefs ++
openapiModel.projectRefs ++
openapiCirce.projectRefs ++
Expand All @@ -90,6 +91,28 @@ lazy val rootProject = (project in file("."))
.settings(publish / skip := true, name := "sttp-apispec", scalaVersion := scala2_13)
.aggregate(projectAggregates: _*)

lazy val circeTestUtils: ProjectMatrix = (projectMatrix in file("circe-testutils"))
.settings(commonSettings)
.settings(
publish / skip := true,
name := "circe-testutils",
libraryDependencies ++= Seq(
"io.circe" %%% "circe-core" % circeVersion,
"io.circe" %%% "circe-parser" % circeVersion
)
)
.jvmPlatform(
scalaVersions = scalaJVMVersions,
settings = commonJvmSettings
)
.jsPlatform(
scalaVersions = scalaJSVersions,
settings = commonJsSettings
)
.nativePlatform(
scalaVersions = scalaNativeVersions,
settings = commonNativeSettings
)
// apispec

lazy val apispecModel: ProjectMatrix = (projectMatrix in file("apispec-model"))
Expand Down Expand Up @@ -134,7 +157,7 @@ lazy val jsonSchemaCirce: ProjectMatrix = (projectMatrix in file("jsonschema-cir
scalaVersions = scalaNativeVersions,
settings = commonNativeSettings
)
.dependsOn(apispecModel)
.dependsOn(apispecModel, circeTestUtils % Test)

// openapi

Expand Down Expand Up @@ -174,7 +197,7 @@ lazy val openapiCirce: ProjectMatrix = (projectMatrix in file("openapi-circe"))
scalaVersions = scalaNativeVersions,
settings = commonNativeSettings
)
.dependsOn(openapiModel, jsonSchemaCirce)
.dependsOn(openapiModel, jsonSchemaCirce, circeTestUtils % Test)

lazy val openapiCirceYaml: ProjectMatrix = (projectMatrix in file("openapi-circe-yaml"))
.settings(commonSettings)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package sttp.apispec.openapi.circe
package sttp.apispec.test

import io.circe.{Json, Error}
import io.circe.parser.decode

trait ResourcePlatform {
def rscPath(path: String): String = "openapi-circe/src/test/resources" + path
def basedir: String
hamnis marked this conversation as resolved.
Show resolved Hide resolved
def rscPath(path: String): String = s"$basedir/src/test/resources$path"

def rsc(path: String): String = {
import scalajs.js.Dynamic.{global => g}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package sttp.apispec.openapi.circe
package sttp.apispec.test

import io.circe._
import io.circe.parser.decode
import java.io.{BufferedReader, InputStreamReader, StringWriter}
import java.nio.charset.StandardCharsets

trait ResourcePlatform {
def basedir: String
def readJson(path: String): Either[Error, Json] = {

val is = getClass.getResourceAsStream(path)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package sttp.apispec.openapi.circe
package sttp.apispec.test

import io.circe._
import io.circe.parser.decode

trait ResourcePlatform {
def rscPath(path: String): String = "openapi-circe/src/test/resources" + path
def basedir: String
def rscPath(path: String): String = s"$basedir/src/test/resources/$path"

def readJson(path: String): Either[Error, Json] = {
val string = scala.io.Source.fromFile(rscPath(path), "UTF-8").mkString
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,21 @@ trait JsonSchemaCirceDecoders {
implicit def listPatternMapDecoder[A: Decoder]: Decoder[ListMap[Pattern, ReferenceOr[A]]] =
Decoder.decodeOption(Decoder.decodeMapLike[Pattern, ReferenceOr[A], ListMap]).map(_.getOrElse(ListMap.empty))

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]] =
Decoder.decodeOption(Decoder.decodeList[A]).map(_.getOrElse(Nil))

def translateDefinitionsTo$def[A](decoder: Decoder[A]) = Decoder.instance { c =>
val modded = c.withFocus(_.mapObject { obj =>
val map = obj.toMap
val definitions = map.get("definitions").orElse(map.get("$defs"))
definitions.map(j => obj.remove("definitions").remove("$defs").add("$defs", j)).getOrElse(obj)
})
decoder.tryDecode(modded)
}

def translateMinMax[A](decoder: Decoder[A]) = Decoder.instance { c =>
val modded = c.withFocus(_.mapObject { obj =>
val map = obj.toMap
Expand All @@ -87,11 +99,13 @@ trait JsonSchemaCirceDecoders {

withExtensions(
translateMinMax(
deriveDecoder[Schema].map(s =>
s.`type` match {
case Some(ArraySchemaType(x :: SchemaType.Null :: Nil)) => s.copy(`type` = Some(x), nullable = Some(true))
case _ => s
}
translateDefinitionsTo$def(
deriveDecoder[Schema].map(s =>
s.`type` match {
case Some(ArraySchemaType(x :: SchemaType.Null :: Nil)) => s.copy(`type` = Some(x), nullable = Some(true))
case _ => s
}
)
)
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ trait JsonSchemaCirceEncoders {
val minKey = if (s.exclusiveMinimum.getOrElse(false)) "exclusiveMinimum" else "minimum"
val maxKey = if (s.exclusiveMaximum.getOrElse(false)) "exclusiveMaximum" else "maximum"
JsonObject(
s"$$id" := s.$id,
s"$$schema" := s.$schema,
"allOf" := s.allOf,
"anyOf" := s.anyOf,
"title" := s.title,
"required" := s.required,
"type" := (if (s.nullable.getOrElse(false))
Expand Down Expand Up @@ -53,6 +55,10 @@ trait JsonSchemaCirceEncoders {
"then" := s.`then`,
"else" := s.`else`,
"$defs" := s.$defs,
"const" := s.const,
"unevaluatedProperties" := s.unevaluatedProperties,
"dependentRequired" := s.dependentRequired,
"dependentSchemas" := s.dependentSchemas,
"extensions" := s.extensions
)
}
Expand Down
32 changes: 32 additions & 0 deletions jsonschema-circe/src/test/resources/extending-recursive.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"$id": "https://example.com/schemas/customer",
"$schema": "https://json-schema.org/draft/2020-12/schema",

"type": "object",
"properties": {
"first_name": { "type": "string" },
"last_name": { "type": "string" },
"shipping_address": { "$ref": "/schemas/address" },
"billing_address": { "$ref": "/schemas/address" }
},
"required": ["first_name", "last_name", "shipping_address", "billing_address"],

"$defs": {
"address": {
"$id": "/schemas/address",
"$schema": "http://json-schema.org/draft-07/schema#",

"type": "object",
"properties": {
"street_address": { "type": "string" },
"city": { "type": "string" },
"state": { "$ref": "#/definitions/state" }
},
"required": ["street_address", "city", "state"],

"definitions": {
"state": { "enum": ["CA", "NY", "... etc ..."] }
}
}
}
}
46 changes: 46 additions & 0 deletions jsonschema-circe/src/test/resources/self-describing-schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"$schema" : "http://iglucentral.com/schemas/com.snowplowanalytics.self-desc/schema/jsonschema/1-0-0#",
"description": "Meta-schema for self-describing JSON schema",
"self": {
"vendor": "com.snowplowanalytics.self-desc",
"name": "schema",
"format": "jsonschema",
"version": "1-0-0"
},

"allOf": [
{
"properties": {
"self": {
"type": "object",
"properties": {
"vendor": {
"type": "string",
"pattern": "^[a-zA-Z0-9-_.]+$"
},
"name": {
"type": "string",
"pattern": "^[a-zA-Z0-9-_]+$"
},
"format": {
"type": "string",
"pattern": "^[a-zA-Z0-9-_]+$"
},
"version": {
"type": "string",
"pattern": "^[0-9]+-[0-9]+-[0-9]+$"
}
},
"required": ["vendor", "name", "format", "version"],
"additionalProperties": false
}
},
"required": ["self"]
},

{
"$ref": "http://json-schema.org/draft-04/schema#"
}
]

}
34 changes: 34 additions & 0 deletions jsonschema-circe/src/test/scala/sttp/apispec/DecoderTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package sttp.apispec

import org.scalatest.funsuite.AnyFunSuite
import sttp.apispec.circe._
import sttp.apispec.test._

import scala.collection.immutable.ListMap

class DecoderTest extends AnyFunSuite with ResourcePlatform {
val basedir = "jsonschema-circe"

test("extending rescursive") {
val Right(json) = readJson("/extending-recursive.json")
val schema = json.as[Schema]
assert(schema.isRight)
val unsafeSchema = schema.right.get
val adrEither = unsafeSchema.$defs.getOrElse(ListMap.empty)("address")

adrEither match {
case s: Schema => assert(s.$schema == Some("http://json-schema.org/draft-07/schema#") && s.$defs.isDefined)
case _ => fail("Nope")
}
}

test("self-decribing-schema") {
val Right(json) = readJson("/self-describing-schema.json")
val schema = json.as[Schema]
assert(schema.isRight)

val unsafeSchema = schema.right.get
assert(unsafeSchema.description === Some("Meta-schema for self-describing JSON schema"))

}
}
17 changes: 17 additions & 0 deletions jsonschema-circe/src/test/scala/sttp/apispec/RoundTripTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package sttp.apispec

import org.scalatest.funsuite.AnyFunSuite
import sttp.apispec.circe._
import sttp.apispec.test._
import io.circe.syntax._
import io.circe.Decoder

class RoundTripTest extends AnyFunSuite with ResourcePlatform {
val basedir = "jsonschema-circe"
hamnis marked this conversation as resolved.
Show resolved Hide resolved

test("Can parse self-encoded schema") {
val simple = Schema($schema = Some("https://json-schema.org/draft/2020-12/schema"), $id = Some("http://yourdomain.com/schemas/myschema.json"))
val decoded = Decoder[Schema].decodeJson(simple.asJson)
assert(decoded.isRight)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ package sttp.apispec
package openapi
package circe

import sttp.apispec.test._
import org.scalatest.funsuite.AnyFunSuite

class DecoderTest extends AnyFunSuite with ResourcePlatform {
val basedir = "openapi-circe"

test("petstore deserialize") {
val Right(openapi) = readJson("/petstore/basic-petstore.json").flatMap(_.as[OpenAPI]): @unchecked

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ package threeone

import io.circe.syntax._
import org.scalatest.funsuite.AnyFunSuite
import sttp.apispec.test._

import scala.collection.immutable.ListMap

class EncoderTest extends AnyFunSuite with ResourcePlatform with circe.SttpOpenAPI3_1CirceEncoders {
val basedir = "openapi-circe"

val petstore: OpenAPI = OpenAPI(
openapi = "3.1.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import sttp.apispec._
import sttp.apispec.openapi._
import org.scalatest.funsuite.AnyFunSuite
import scala.collection.immutable.ListMap
import sttp.apispec.openapi.circe.ResourcePlatform
import sttp.apispec.test._

class EncoderTest extends AnyFunSuite with ResourcePlatform {
val basedir = "openapi-circe"
def refOr[A](a: A): ReferenceOr[A] = Right(a)

test("any boolean") {
Expand Down