Skip to content

Commit

Permalink
- preserve object field order (#298)
Browse files Browse the repository at this point in the history
- preserve object field order
- introduce schema equality framework
- introduce schema matcher to me used in tests for detailed difference presentation
  • Loading branch information
andyglow authored Jun 29, 2023
1 parent d70e65a commit b3f5511
Show file tree
Hide file tree
Showing 10 changed files with 390 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import json.{Json, Schema}
import json.schema.validation.Instance._
import json.Schema._
import json.schema.Version.Raw
//import json.schema._
import org.scalatest.matchers.should.Matchers._
import org.scalatest.wordspec.AnyWordSpec
import SchemaMatchers._

class SchemaMacroSpec extends AnyWordSpec {
import SchemaMacroSpec._
Expand All @@ -16,34 +16,37 @@ class SchemaMacroSpec extends AnyWordSpec {
"generate schema for case class of primitive fields" in {
import `object`.Field

val expected = `object`(Field("name", `string`), Field("bar", `integer`))
val expected = `object`(
Field("name", `string`),
Field("bar", `integer`)
)

Json.schema[Foo1] shouldEqual expected
Json.schema[Foo1] should matchSchema(expected)
}

"generate schema for case class of optional primitive fields" in {
import `object`.Field

Json.schema[Foo2] shouldEqual `object`(
Json.schema[Foo2] should matchSchema(`object`(
Field("name", `string`, required = false),
Field("bar", `integer`, required = false)
)
))
}

"generate schema for case class of primitive fields with default values" in {
import `object`.Field

Json.schema[Foo3] shouldEqual `object`(
Json.schema[Foo3] should matchSchema(`object`(
Field("name", `string`, required = false, default = "xxx"),
Field("bar", `integer`, required = false, default = 5),
Field("active", `boolean`, required = false, default = true)
)
))
}

"generate schema for case class of array fields with default values" in {
import `object`.Field

Json.schema[Bar9] shouldEqual `object`(
Json.schema[Bar9] should matchSchema(`object`(
Field("set", `array`(`integer`, unique = true), required = false, default = Set(1, 5, 9)),
Field("list", `array`(`boolean`), required = false, default = List(true, false)),
Field("vector", `array`(`number`[Long]), required = false, default = Vector(9, 7)),
Expand All @@ -60,7 +63,7 @@ class SchemaMacroSpec extends AnyWordSpec {
required = false,
default = Map(1 -> "1", 2 -> "2")
)
)
))
}

"generate references for implicitly defined dependencies" in {
Expand All @@ -76,7 +79,7 @@ class SchemaMacroSpec extends AnyWordSpec {

val schema = Json.schema[Foo4]

schema shouldEqual `object`(
schema should matchSchema(`object`(
Field(
"component",
`def`[Compo1](
Expand All @@ -85,7 +88,7 @@ class SchemaMacroSpec extends AnyWordSpec {
),
required = true
)
)
))
}
}

Expand All @@ -97,17 +100,19 @@ class SchemaMacroSpec extends AnyWordSpec {
}

Json
.schema[WeekDay.type] shouldEqual `enum`.of("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
Json.schema[WeekDay.Value] shouldEqual `enum`.of(
.schema[WeekDay.type] should matchSchema(`enum`.of("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"))

Json.schema[WeekDay.Value] should matchSchema(`enum`.of(
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
"Sun"
)
Json.schema[WeekDay.T] shouldEqual `enum`.of("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
))

Json.schema[WeekDay.T] should matchSchema(`enum`.of("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"))

object Planet extends Enumeration {
protected case class PlanetVal(mass: Double, radius: Double) extends super.Val {
Expand All @@ -128,7 +133,7 @@ class SchemaMacroSpec extends AnyWordSpec {
val Neptune = PlanetVal(1.024e+26, 2.4746e7)
}

Json.schema[Planet.type] shouldEqual `enum`.of(
Json.schema[Planet.type] should matchSchema(`enum`.of(
"Mercury",
"Venus",
"Earth",
Expand All @@ -137,11 +142,11 @@ class SchemaMacroSpec extends AnyWordSpec {
"Saturn",
"Uranus",
"Neptune"
)
))

Json.schema[Map[WeekDay.type, String]] shouldEqual `dictionary`[WeekDay.type, String, Map](
Json.schema[Map[WeekDay.type, String]] should matchSchema(`dictionary`[WeekDay.type, String, Map](
`string`
).withValidation(`patternProperties` := "^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)$")
).withValidation(`patternProperties` := "^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)$"))
}

"generate schema for Sealed Trait Enums" in {
Expand All @@ -162,122 +167,122 @@ class SchemaMacroSpec extends AnyWordSpec {
Color.Green
Color.Blue

Json.schema[Color] shouldEqual `enum`.of("Red", "Green", "Blue")
Json.schema[Color] should matchSchema(`enum`.of("Red", "Green", "Blue"))

Json.schema[Map[Color, String]] shouldEqual `dictionary`[Color, String, Map](`string`)
.withValidation(`patternProperties` := "^(?:Red|Green|Blue)$")
Json.schema[Map[Color, String]] should matchSchema(`dictionary`[Color, String, Map](`string`)
.withValidation(`patternProperties` := "^(?:Red|Green|Blue)$"))
}

"generate schema for Sealed Trait subclasses" in {
import `object`.Field

Json.schema[FooBar] shouldEqual `oneof`(
Json.schema[FooBar] should matchSchema(`oneof`(
Set(`object`(Field("foo", `number`[Double])), `object`(Field("bar", `number`[Double])))
)
))
}

"generate schema for Multi Level Sealed Trait subclasses" in {
import `object`.Field

Json.schema[MultiLevelSealedTraitRoot] shouldEqual `oneof`(
Json.schema[MultiLevelSealedTraitRoot] should matchSchema(`oneof`(
Set(
`object`(Field("a", `integer`)),
`object`(Field("b", `string`)),
`object`(Field("c", `boolean`)),
`object`(Field("d", `number`[Double])),
`object`(Field("e", `array`[String, List](`string`)))
)
)
))
}

"generate schema for Sealed Trait subclasses defined inside of it's companion object" in {
import `object`.Field

Json.schema[FooBarInsideCompanion] shouldEqual `oneof`(
Json.schema[FooBarInsideCompanion] should matchSchema(`oneof`(
Set(`object`(Field("foo", `number`[Double])), `object`(Field("bar", `number`[Double])))
)
))
}

"generate schema for hybrid Sealed Trait family" in {
import `object`.Field

Json.schema[HybridSum] shouldEqual `oneof`(
Json.schema[HybridSum] should matchSchema(`oneof`(
Set(`object`(Field("id", `integer`), Field("name", `string`)), `enum`.of("V2", "V3"))
)
))
}

"generate schema for hybrid generic Sealed Trait family" in {
import `object`.Field

Json.schema[HybridGenericSum[Double]] shouldEqual `oneof`(
Json.schema[HybridGenericSum[Double]] should matchSchema(`oneof`(
Set(`object`(Field("id", `integer`), Field("value", `number`[Double])), `enum`.of("V2"))
)
))
}

"generate schema for hybrid recursive Sealed Trait family" in {
import `object`.Field

Json.schema[HybridRecursiveSum] shouldEqual `oneof`(
Json.schema[HybridRecursiveSum] should matchSchema(`oneof`(
Set(
`object`(Field("intVal", `integer`), Field("err", `string`, required = false)),
`object`(Field("err", `string`, required = false), Field("intVal", `integer`)),
`object`(Field("tpe", `string`)),
`object`(Field("error", `string`)),
`object`(Field("value", `integer`)),
`enum`.of("L1V3", "L2V1", "L2V2")
)
)
))
}

"generate schema for Map which Sealed Values Family for values" in {

Json.schema[Map[String, AnyFooBar]] shouldEqual `dictionary`[String, AnyFooBar, Map](
Json.schema[Map[String, AnyFooBar]] should matchSchema(`dictionary`[String, AnyFooBar, Map](
`oneof`.of(`value-class`(`string`), `value-class`(`integer`))
)
))
}

"generate schema for List which Sealed Values Family for values" in {

Json.schema[List[AnyFooBar]] shouldEqual `array`[AnyFooBar, List](
Json.schema[List[AnyFooBar]] should matchSchema(`array`[AnyFooBar, List](
`oneof`.of(`value-class`(`string`), `value-class`(`integer`))
)
))
}

"generate schema for case class that includes another case class" in {
import `object`.Field

Json.schema[Bar5] shouldEqual `object`(
Json.schema[Bar5] should matchSchema(`object`(
Field("foo", `object`(Field("name", `string`), Field("bar", `integer`)))
)
))
}

"generate schema for case class using collection of string" in {
import `object`.Field

Json.schema[Bar6] shouldEqual `object`(Field("foo", `array`(`string`)))
Json.schema[Bar6] should matchSchema(`object`(Field("foo", `array`(`string`))))
}

"generate schema for case class using collection of integers" in {
import `object`.Field

Json.schema[Bar7] shouldEqual `object`(Field("foo", `array`(`integer`)))
Json.schema[Bar7] should matchSchema(`object`(Field("foo", `array`(`integer`))))
}

"generate schema for value class" in {
Json.schema[Bar8] shouldEqual `value-class`[Bar8, String](`string`)
Json.schema[Bar8] should matchSchema(`value-class`[Bar8, String](`string`))
}

"generate schema for Map[String, _]" in {
import `object`.Field

Json.schema[Map[String, String]] shouldEqual `dictionary`(`string`)
Json.schema[Map[String, String]] should matchSchema(`dictionary`(`string`))

Json.schema[Map[String, Int]] shouldEqual `dictionary`(`integer`)
Json.schema[Map[String, Int]] should matchSchema(`dictionary`(`integer`))

Json.schema[Map[String, Foo9]] shouldEqual `dictionary`[String, Foo9, Map](
Json.schema[Map[String, Foo9]] should matchSchema(`dictionary`[String, Foo9, Map](
`object`(Field("name", `string`))
)
))

Json.schema[Map[Bar8, Int]] shouldEqual `dictionary`[Bar8, Int, Map](`integer`)
Json.schema[Map[Bar8, Int]] should matchSchema(`dictionary`[Bar8, Int, Map](`integer`))

import com.github.andyglow.json.Value._

Expand All @@ -292,22 +297,22 @@ class SchemaMacroSpec extends AnyWordSpec {
"generate schema for Map[_: MapKeyPattern, _]" in {
import `object`.Field

Json.schema[Map[Long, Long]] shouldEqual `dictionary`[Long, Long, Map](`number`[Long])
.withValidation(`patternProperties` := "^[0-9]+$")
Json.schema[Map[Long, Long]] should matchSchema(`dictionary`[Long, Long, Map](`number`[Long])
.withValidation(`patternProperties` := "^[0-9]+$"))

Json.schema[Map[Char, Long]] shouldEqual `dictionary`[Char, Long, Map](`number`[Long])
.withValidation(`patternProperties` := "^.{1}$")
Json.schema[Map[Char, Long]] should matchSchema(`dictionary`[Char, Long, Map](`number`[Long])
.withValidation(`patternProperties` := "^.{1}$"))

Json.schema[Map[Int, String]] shouldEqual `dictionary`[Int, String, Map](`string`)
.withValidation(`patternProperties` := "^[0-9]+$")
Json.schema[Map[Int, String]] should matchSchema(`dictionary`[Int, String, Map](`string`)
.withValidation(`patternProperties` := "^[0-9]+$"))

Json.schema[Map[Int, Int]] shouldEqual `dictionary`[Int, Int, Map](`integer`).withValidation(
Json.schema[Map[Int, Int]] should matchSchema(`dictionary`[Int, Int, Map](`integer`).withValidation(
`patternProperties` := "^[0-9]+$"
)
))

Json.schema[Map[Int, Foo9]] shouldEqual `dictionary`[Int, Foo9, Map](
Json.schema[Map[Int, Foo9]] should matchSchema(`dictionary`[Int, Foo9, Map](
`object`(Field("name", `string`))
).withValidation(`patternProperties` := "^[0-9]+$")
).withValidation(`patternProperties` := "^[0-9]+$"))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.github.andyglow.jsonschema

import com.github.andyglow.jsonschema.SchemaEquality.{ Equal, UnEqual }
import json.Schema
import org.scalatest.enablers.Sequencing
import org.scalatest.matchers.{MatchResult, Matcher}

class SchemaMatchers(right: Schema[_], checkOrder: Boolean) extends Matcher[Schema[_]] {

override def apply(left: Schema[_]): MatchResult = {
import Schema._, `object`.Field

val eq = SchemaEquality(checkOrder).compute(left, right)
val explanation = eq match {
case Equal => ""
case UnEqual(diff) => " due to " + diff.toDebugString
}

MatchResult(
eq == Equal,
left.toDebugString + " was not equal to " + right.toDebugString + explanation,
left.toDebugString + " was equal to " + right.toDebugString
)
}
}

object SchemaMatchers {

def matchSchema(x: Schema[_]) = new SchemaMatchers(x, checkOrder = true)

def unorderedMatchSchema(x: Schema[_]) = new SchemaMatchers(x, checkOrder = false)
}
10 changes: 10 additions & 0 deletions core/src/main/scala-2.11/com/github/andyglow/scalamigration.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ object scalamigration {
implicit class EitherOps[L, R](private val x: Either[L, R]) extends AnyVal {

@inline def opt: Option[R] = x.right.toOption

@inline def flatMap[L1 >: L, R1](f: R => Either[L1, R1]): Either[L1, R1] = x match {
case Right(b) => f(b)
case _ => x.asInstanceOf[Either[L1, R1]]
}

@inline def map[R1](f: R => R1): Either[L, R1] = x match {
case Right(b) => Right(f(b))
case _ => x.asInstanceOf[Either[L, R1]]
}
}

implicit class MapMigOps[K, V](private val x: Map[K, V]) extends AnyVal {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ trait AsDraftSupport {
("title", x.title),
("additionalProperties", canHaveAdditionalProperties),
("properties", if (props.isEmpty) None else Some(obj(props))),
("required", if (required.isEmpty) None else Some(arr(required.toSeq)))
("required", if (required.isEmpty) None else Some(arr(required.distinct)))
)
}

Expand Down
Loading

0 comments on commit b3f5511

Please sign in to comment.