Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -161,14 +161,6 @@ class AsDraft04Spec extends AnyWordSpec {
"type" -> "string")))
}

"emit Map[Int, _]" in {
asDraft04(`int-map`(`string`[String](None, None))) shouldEqual obj(
"type" -> "object",
"patternProperties" -> obj(
"^[0-9]*$" -> obj(
"type" -> "string")))
}

"emit Object" in {
import `object`.Field

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.github.andyglow.jsonschema

import json._
import json.Validation._
import json.Schema._
import json.Schema.`object`.Field

Expand All @@ -17,7 +18,7 @@ class CRLFSourceSpec extends AnyFunSuite {
Field("list" , `array`(`boolean`), required = false, default = List(true, false)),
Field("vector" , `array`(`number`[Long]), required = false, default = Vector(9, 7)),
Field("strMap" , `string-map`(`number`[Double]), required = false, default = Map("foo" -> .12)),
Field("intMap" , `int-map`(`string`[String](None, None)), required = false, default = Map(1 -> "1", 2 -> "2")))
Field("intMap" , `string-map`[Int, String, Map](`string`[String](None, None)).withValidation(`patternProperties` := "^[0-9]+$"), required = false, default = Map(1 -> "1", 2 -> "2")))
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.github.andyglow.jsonschema

import json.{Json, Schema}
import json.Validation._
import json.Schema._
import org.scalatest.matchers.should.Matchers._
import org.scalatest.wordspec.AnyWordSpec
Expand Down Expand Up @@ -45,7 +46,7 @@ class SchemaMacroSpec extends AnyWordSpec {
Field("list" , `array`(`boolean`), required = false, default = List(true, false)),
Field("vector" , `array`(`number`[Long]), required = false, default = Vector(9, 7)),
Field("strMap" , `string-map`(`number`[Double]), required = false, default = Map("foo" -> .12)),
Field("intMap" , `int-map`(`string`[String](None, None)), required = false, default = Map(1 -> "1", 2 -> "2")))
Field("intMap" , `string-map`[Int, String, Map](`string`[String](None, None)).withValidation(`patternProperties` := "^[0-9]+$"), required = false, default = Map(1 -> "1", 2 -> "2")))
}

"generate references for implicitly defined dependencies" in {
Expand Down Expand Up @@ -120,20 +121,20 @@ class SchemaMacroSpec extends AnyWordSpec {
}


"generate schema for Map which Sealed Family for values" in {
import `object`.Field

Json.schema[Map[String, FooBar]] shouldEqual `string-map`(
`oneof`(Set(
`object`(Field("foo", `number`[Double]())),
`object`(Field("bar", `number`[Double]())))))
}
// "generate schema for Map which Sealed Family for values" in {
// import `object`.Field
//
// Json.schema[Map[String, FooBar]] shouldEqual `string-map`[String, FooBar, Map](
// `oneof`(Set(
// `object`(Field("foo", `number`[Double]())),
// `object`(Field("bar", `number`[Double]())))))
// }

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

Json.schema[Map[String, AnyFooBar]] shouldEqual `string-map`(
`oneof`(Set(`string`[String](None, None), `integer`)))
}
// "generate schema for Map which Sealed Values Family for values" in {
//
// Json.schema[Map[String, AnyFooBar]] shouldEqual `string-map`[String, AnyFooBar, Map](
// `oneof`(Set(`string`[String](None, None), `integer`)))
// }

"generate schema for case class using another case class" in {
import `object`.Field
Expand Down Expand Up @@ -169,21 +170,23 @@ class SchemaMacroSpec extends AnyWordSpec {

Json.schema[Map[String, Int]] shouldEqual `string-map`(`integer`)

Json.schema[Map[String, Foo9]] shouldEqual `string-map`(`object`(Field("name", `string`[String](None, None))))
Json.schema[Map[String, Foo9]] shouldEqual `string-map`[String, Foo9, Map](`object`(Field("name", `string`[String](None, None))))
}


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

Json.schema[Map[Int, String]] shouldEqual `int-map`(`string`[String](None, None))
Json.schema[Map[Long, Long]] shouldEqual `string-map`[Long, Long, Map](`number`[Long]).withValidation(`patternProperties` := "^[0-9]+$")

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

Json.schema[Map[Int, Int]] shouldEqual `int-map`(`integer`)
Json.schema[Map[Int, String]] shouldEqual `string-map`[Int, String, Map](`string`[String](None, None)).withValidation(`patternProperties` := "^[0-9]+$")

Json.schema[Map[Int, Foo9]] shouldEqual `int-map`(`object`(Field("name", `string`[String](None, None))))
Json.schema[Map[Int, Int]] shouldEqual `string-map`[Int, Int, Map](`integer`).withValidation(`patternProperties` := "^[0-9]+$")

Json.schema[Map[Int, Foo9]] shouldEqual `string-map`[Int, Foo9, Map](`object`(Field("name", `string`[String](None, None)))).withValidation(`patternProperties` := "^[0-9]+$")
}
}

}

object SchemaMacroSpec {
Expand Down
1 change: 0 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,6 @@ lazy val enumeratum = { project in file("modules/enumeratum") }.dependsOn(core,
name := "scala-jsonschema-enumeratum",

libraryDependencies += "com.beachape" %% "enumeratum" % "1.6.1",
libraryDependencies += (scalaVersion apply ("org.scala-lang" % "scala-reflect" % _ % Compile)).value.withSources.withJavadoc
)

lazy val root = { project in file(".") }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,6 @@ trait AsDraftSupport {
pattern -> apply(comp)))
}

def mkIntMap(pp: Option[ValidationDef[_, _]], comp: Schema[_]): obj = {
obj("patternProperties" -> obj(
"^[0-9]*$" -> apply(comp)))
}

def mkArr(pp: Option[ValidationDef[_, _]], comp: Schema[_]): obj = {
obj("items" -> apply(comp))
}
Expand Down Expand Up @@ -119,7 +114,6 @@ trait AsDraftSupport {
case (pp, x: `string`[_]) => mkStr(pp, x)
case (pp, x: `object`[_]) => mkObj(pp, x)
case (pp, `string-map`(comp)) => mkStrMap(pp, comp)
case (pp, `int-map`(comp)) => mkIntMap(pp, comp)
case (pp, `array`(comp)) => mkArr(pp, comp)
case (pp, `set`(comp)) => mkSet(pp, comp)
case (pp, x: `enum`[_]) => mkEnum(pp, x)
Expand Down
26 changes: 11 additions & 15 deletions core/src/main/scala/json/Schema.scala
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,9 @@ object Schema {
}
}

final case class `string-map`[T, C[_ <: String, _]](
valueType: Schema[T]) extends Schema[C[String, T]] {
final case class `string-map`[K, V, C[_, _]](valueType: Schema[V]) extends Schema[C[K, V]] {
override def jsonType = "object"
def mkCopy() = new `string-map`[T, C](valueType)
def mkCopy() = new `string-map`[K, V, C](valueType)
override def canEqual(that: Any): Boolean = that match {
case `string-map`(_) => true
case _ => false
Expand All @@ -186,18 +185,15 @@ object Schema {
case _ => false
}
}

final case class `int-map`[T, C[_ <: Int, _]](
valueType: Schema[T]) extends Schema[C[Int, T]] {
override def jsonType = "object"
def mkCopy() = new `int-map`[T, C](valueType)
override def canEqual(that: Any): Boolean = that match {
case `int-map`(_) => true
case _ => false
}
override def equals(obj: Any): Boolean = obj match {
case `int-map`(c) => valueType == c && super.equals(obj)
case _ => false
final object `string-map` {
abstract class MapKeyPattern[T](val pattern: String)
final object MapKeyPattern {
implicit final object StringRE extends MapKeyPattern[String]("^.*$")
implicit final object CharRE extends MapKeyPattern[Char]("^.{1}$")
implicit final object IntRE extends MapKeyPattern[Int]("^[0-9]+$")
implicit final object LongRE extends MapKeyPattern[Long]("^[0-9]+$")
// enums
// ^(?:aaa|bbb|ccc)$
}
}

Expand Down
11 changes: 8 additions & 3 deletions core/src/main/scala/json/schema/Predef.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ package json.schema
import java.net.{URI, URL}
import java.util.UUID

import json.Schema
import json.{Schema, Validation}
import json.Schema._
import json.Schema.`string-map`.MapKeyPattern
import json.Schema.`string`.Format
import json.Validation._

Expand Down Expand Up @@ -54,6 +55,10 @@ object Predef extends LowPriorityPredefs {
implicit def setS[T](implicit p: Predef[T]): Predef[Set[T]] = Predef(`set`[T, Set](p.schema))
implicit def listS[T](implicit p: Predef[T]): Predef[List[T]] = Predef(`array`[T, List](p.schema))
implicit def vectorS[T](implicit p: Predef[T]): Predef[Vector[T]] = Predef(`array`[T, Vector](p.schema))
implicit def strMapS[T](implicit p: Predef[T]): Predef[Map[String, T]] = Predef(`string-map`[T, Map](p.schema))
implicit def intMapS[T](implicit p: Predef[T]): Predef[Map[Int, T]] = Predef(`int-map`[T, Map](p.schema))
implicit def strMapS[K, V](implicit p: Predef[V], keyP: MapKeyPattern[K]): Predef[Map[K, V]] = Predef {
val schema = `string-map`[K, V, Map](p.schema)
if (keyP == `string-map`.MapKeyPattern.StringRE) schema else {
schema withValidation (Validation.`patternProperties` := keyP.pattern)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.github.andyglow.jsonschema

import com.github.andyglow.json.ToValue
import com.github.andyglow.scaladoc.{Scaladoc, SlowParser}
import json.Schema.`string-map`.MapKeyPattern

import scala.reflect.NameTransformer
import scala.reflect.internal.util.NoSourceFile
Expand All @@ -21,16 +22,19 @@ object SchemaMacro {
def impl[T: c.WeakTypeTag](c: blackbox.Context): c.Expr[json.Schema[T]] = {
import c.universe._

val jsonPkg = q"_root_.json"
val intJsonPkg = q"_root_.com.github.andyglow.json"
val schemaObj = q"$jsonPkg.Schema"
val jsonPkg = q"_root_.json"
val intJsonPkg = q"_root_.com.github.andyglow.json"
val schemaObj = q"$jsonPkg.Schema"
val validationObj = q"$jsonPkg.Validation"

val subject = weakTypeOf[T]
val optionTpe = weakTypeOf[Option[_]]
val toValueTpe = weakTypeOf[ToValue[_]]
val setTpe = weakTypeOf[Set[_]]
val mapTpe = weakTypeOf[Map[_, _]]
val schemaTypeConstructor = typeOf[json.Schema[_]].typeConstructor
val predefTypeConstructor = typeOf[json.schema.Predef[_]].typeConstructor
val mapKeyPattern = typeOf[MapKeyPattern[_]]

def getTypeScaladoc(tpe: Type): Option[Scaladoc] = {
import com.github.andyglow.scalamigration._
Expand Down Expand Up @@ -321,23 +325,35 @@ object SchemaMacro {
}
}

object IntMap {
case class StringMapGen(tpe: Type, keyType: Type, valueType: Type, keyPattern: Tree) {
private val stringKey = keyType <:< typeOf[String]

def gen(tpe: Type, stack: List[Type]): Tree = {
val componentType = tpe.typeArgs.tail.head
val componentJsonType = resolve(componentType, tpe +: stack)
def gen(stack: List[Type]): Tree = {
val valueJsonType = resolve(valueType, tpe +: stack)
val tree = q"""$schemaObj.`string-map`[$keyType, $valueType, ${tpe.typeConstructor}]($valueJsonType)"""
val effectiveTree = if (!stringKey) {
q"""$tree withValidation ($validationObj.patternProperties := $keyPattern.pattern)"""
} else tree

q"""$schemaObj.`int-map`[$componentType, ${tpe.typeConstructor}]($componentJsonType)"""
effectiveTree
}
}

object StringMap {

def gen(tpe: Type, stack: List[Type]): Tree = {
val componentType = tpe.typeArgs.tail.head
val componentJsonType = resolve(componentType, tpe +: stack)
def unapply(x: Type): Option[StringMapGen] = {
if (x <:< mapTpe) {
val keyType = x.typeArgs.head
val valueType = x.typeArgs.tail.head
val t = appliedType(mapKeyPattern, keyType)

q"""$schemaObj.`string-map`[$componentType, ${tpe.typeConstructor}]($componentJsonType)"""
c.inferImplicitValue(t) match {
case EmptyTree => None
case t => Some(StringMapGen(x, keyType, valueType, t))
}
} else {
None
}
}
}

Expand Down Expand Up @@ -439,8 +455,7 @@ object SchemaMacro {
if (stack contains tpe) c.error(c.enclosingPosition, s"cyclic dependency for $tpe")

def genTree: Tree = tpe match {
case x if x <:< typeOf[Map[String, _]] => StringMap.gen(x, stack)
case x if x <:< typeOf[Map[Int, _]] => IntMap.gen(x, stack)
case StringMap(g) => g.gen(stack)
case x if x <:< typeOf[Array[_]] => Arr.gen(x, stack)
case x if x <:< typeOf[Iterable[_]] => Arr.gen(x, stack)
case SealedEnum(names) => SealedEnum.gen(tpe, names)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.github.andyglow.jsonschema
import cats.data._
import json.Schema
import json.Schema._
import json.Schema.`string-map`.MapKeyPattern
import json.Validation._
import json.schema.Predef

Expand All @@ -15,13 +16,16 @@ trait LowPriorityCatsSupport extends ScalaVersionSpecificLowPriorityCatsSupport
implicit def nevVB[X]: ValidationBound[NonEmptyVector[X], Iterable[_]] = mk[NonEmptyVector[X], Iterable[_]]
implicit def nesVB[X]: ValidationBound[NonEmptySet[X], Iterable[_]] = mk[NonEmptySet[X], Iterable[_]]
implicit def necVB[X]: ValidationBound[NonEmptyChain[X], Iterable[_]] = mk[NonEmptyChain[X], Iterable[_]]
implicit def nemStrVB[X]: ValidationBound[NonEmptyMap[String, X], Map[_, _]] = mk[NonEmptyMap[String, X], Map[_, _]]
implicit def nemIntVB[X]: ValidationBound[NonEmptyMap[Int, X], Map[_, _]] = mk[NonEmptyMap[Int, X], Map[_, _]]
implicit def nemStrVB[K, V]: ValidationBound[NonEmptyMap[K, V], Map[_, _]] = mk[NonEmptyMap[K, V], Map[_, _]]
implicit def oneAndVB[F[_], X]: ValidationBound[OneAnd[F, X], Iterable[_]] = mk[OneAnd[F, X], Iterable[_]]

protected def mkNEx[T, C[_]](schema: Schema[T])(implicit b: ValidationBound[C[T], Iterable[_]]) = Predef(`array`[T, C](schema).withValidation(`minItems` := 1))
protected def mkNESM[T](schema: Schema[T])(implicit b: ValidationBound[NonEmptyMap[String, T], Map[_, _]]) = Predef(`string-map`[T, NonEmptyMap](schema).withValidation(`minProperties` := 1))
protected def mkNEIM[T](schema: Schema[T])(implicit b: ValidationBound[NonEmptyMap[Int, T], Map[_, _]]) = Predef(`int-map`[T, NonEmptyMap](schema).withValidation(`minProperties` := 1))
protected def mkNESM[K, V](vSchema: Schema[V], keyP: MapKeyPattern[K])(implicit b: ValidationBound[NonEmptyMap[K, V], Map[_, _]]) = Predef {
val schema = `string-map`[K, V, NonEmptyMap](vSchema).withValidation(`minProperties` := 1)
if (keyP == `string-map`.MapKeyPattern.StringRE) schema else {
schema.withValidation(`patternProperties` := keyP.pattern)
}
}

implicit def chainSchemaFromPredef[T](implicit p: Predef[T]): Predef[Chain[T]] = mkNEx[T, Chain](p.schema)

Expand All @@ -35,9 +39,7 @@ trait LowPriorityCatsSupport extends ScalaVersionSpecificLowPriorityCatsSupport

implicit def necSchemaFromPredef[T](implicit p: Predef[T]): Predef[NonEmptyChain[T]] = mkNEx[T, NonEmptyChain](p.schema)

implicit def nemStrSchemaFromPredef[T](implicit p: Predef[T]): Predef[NonEmptyMap[String, T]] = mkNESM(p.schema)

implicit def nemIntSchemaFromPredef[T](implicit p: Predef[T]): Predef[NonEmptyMap[Int, T]] = mkNEIM(p.schema)
implicit def nemStrSchemaFromPredef[K, V](implicit p: Predef[V], keyP: MapKeyPattern[K]): Predef[NonEmptyMap[K, V]] = mkNESM(p.schema, keyP)
}

object CatsSupport extends LowPriorityCatsSupport with ScalaVersionSpecificCatsSupport {
Expand All @@ -54,7 +56,6 @@ object CatsSupport extends LowPriorityCatsSupport with ScalaVersionSpecificCatsS

implicit def necSchema[T](implicit ss: Schema[T]): Predef[NonEmptyChain[T]] = mkNEx[T, NonEmptyChain](ss)

implicit def nemStrSchema[T](implicit ss: Schema[T]): Predef[NonEmptyMap[String, T]] = mkNESM(ss)
implicit def nemStrSchema[K, V](implicit ss: Schema[V], keyP: MapKeyPattern[K]): Predef[NonEmptyMap[K, V]] = mkNESM(ss, keyP)

implicit def nemIntSchema[T](implicit ss: Schema[T]): Predef[NonEmptyMap[Int, T]] = mkNEIM(ss)
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class CatsSupportSpec extends AnyWordSpec {
"be exposed as object" in {
nesmEventSchema shouldBe `object`(
Field("id", `string`()),
Field("data" , `string-map`[String, NonEmptyMap](`string`()).withValidation(`minProperties` := 1)))
Field("data" , `string-map`[String, String, NonEmptyMap](`string`()).withValidation(`minProperties` := 1)))
}
}

Expand All @@ -65,7 +65,7 @@ class CatsSupportSpec extends AnyWordSpec {
"be exposed as object" in {
neimEventSchema shouldBe `object`(
Field("id", `string`()),
Field("data", `int-map`[String, NonEmptyMap](`string`()).withValidation(`minProperties` := 1)))
Field("data", `string-map`[Int, String, NonEmptyMap](`string`()).withValidation(`minProperties` := 1, `patternProperties` := "^[0-9]+$")))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import java.io.{ByteArrayInputStream, InputStream}
import scala.collection._
import com.github.andyglow.json.{ParseJson, Value}
import json.Schema
import json.Validation._

import scala.util.{Failure, Success, Try}

Expand Down Expand Up @@ -78,9 +79,10 @@ object ParseJsonSchema {
}

def makeObj = x.value.obj("patternProperties") match {
case Some(obj(fields)) if fields.contains("^.*$") => makeType(fields.obj("^.*$").get) map { `string-map`(_) }
case Some(obj(fields)) if fields.contains("^[0-9]*$") => makeType(fields.obj("^[0-9]*$").get) map { `int-map`(_) }
case None =>
case Some(obj(fields)) if fields.nonEmpty && fields.head._2.isInstanceOf[obj] =>
val (k, v) = fields.head
makeType(v.asInstanceOf[obj]) map { x => `string-map`[Any, Any, scala.collection.immutable.Map](x).withValidation(`patternProperties` := k) }
case _ =>
val required = x.value.set("required") map { _ collect { case str(x) => x } } getOrElse Set.empty
x.value.obj("properties").map { _.value }.toSuccess("properties is not defined") flatMap { props =>
val fields = props.collect { case (k, v: obj) =>
Expand Down