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

#6 Flesh out the GenModule #19

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ jdk:
before_install:
- export PATH=${PATH}:./vendor/bundle
script:
- sbt ++$TRAVIS_SCALA_VERSION scalafmtCheck test:scalafmtCheck scalafmtSbtCheck test:run
- sbt ++$TRAVIS_SCALA_VERSION scalafmtCheck test:scalafmtCheck scalafmtSbtCheck "project core" test:run "project scalacheck" test:run

37 changes: 37 additions & 0 deletions DESIGN_NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# scalaz-schema design notes
This document aims to lay out explanations for every scalaz-schema major design change.


## Free Applicative for record fields

At first we defined record schema as nonempty list of record fields that looked like:
```
sealed case class RecordSchema[A](terms: NonEmptyList[Field[A, _]]) extends Schema[A]
```
During development of `GenModule` we discovered that definition was not expressive enough.
Imagine that you have person record that looks like:
```
case class Person(name: String, age: Int)
```
and you want to generate `Gen[Person]` out of your `Schema[Person]`.
If you do it by hand first thing what you would do is create gens for person fields,
`Gen[String]` and `Gen[Int]`.
Now if `Gen` is applicative it is relatively easy to create `Gen[Person]` out of gens for fields.
You could create it if `Gen` is monad but applicative fits here naturally since you don't need sequential
computation to create this object. You are done now.

But what to do if you want to have generic implementation?
By just looking at record schema for type `A` and list of it fields you don't know how to create `A`
if you have values of its fields. That is why we use free applicative that has fields `Field[A, A0]`
for its algebra and produces `A`:
```
sealed case class RecordSchema[A](fields: FreeAp[Field[A, ?], A]) extends Schema[A]
```

If you have free applicative you can easily interpret it to some other type constructor
`F[_]` if `F` is applicative using `foldMap` method that has signature:
```
def foldMap[G[_]:Applicative](f: F ~> G): G[A]
```
That is what we did in [Gen module](https://github.com/scalaz/scalaz-schema/pull/19/files#diff-b36b3e5424683afebef8984ee076d92fR35).
We defined applicative for gen, natural transformation from field algebra to gen and used foldMap to get gen for record type.
49 changes: 41 additions & 8 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,21 +1,54 @@
lazy val scalaz =
ProjectRef(uri("git:https://github.com/scalaz/scalaz.git#series/8.0.x"), "baseJVM")
ProjectRef(
uri("git:https://github.com/scalaz/scalaz.git#series/8.0.x"),
"baseJVM"
)

val testzVersion = "0.0.4"
val monocleVersion = "1.5.0"

lazy val root = project
.in(file("."))
.settings(
name := "scalaz-schema",
name := "scalaz-schema"
)
.aggregate(
core,
scalacheck,
`test-commons`
)
.dependsOn(scalaz)

lazy val core = project
.in(file("modules/core"))
.settings(
name := "scalaz-schema-core",
libraryDependencies ++= Seq(
"com.github.julien-truffaut" %% "monocle-core" % monocleVersion,
"com.github.julien-truffaut" %% "monocle-macro" % monocleVersion,
"org.scalaz" %% "testz-core" % testzVersion % "test",
"org.scalaz" %% "testz-stdlib" % testzVersion % "test",
"org.scalaz" %% "testz-runner" % testzVersion % "test"
"com.github.julien-truffaut" %% "monocle-macro" % monocleVersion
).map(_.exclude("org.scalaz", "scalaz"))
)
.dependsOn(scalaz)
.dependsOn(`test-commons` % "test->test")

lazy val scalacheck = project.in(file("modules/scalacheck")).dependsOn(root)
lazy val scalacheck = project
.in(file("modules/scalacheck"))
.settings(
name := "scalaz-schema-scalacheck",
libraryDependencies ++= Seq(
"org.scalacheck" %% "scalacheck" % "1.14.0"
)
)
.dependsOn(core, `test-commons` % "test->test")

lazy val `test-commons` = project
.in(file("modules/test-commons"))
.settings(
name := "scalaz-test-commons",
libraryDependencies ++= Seq(
"com.github.julien-truffaut" %% "monocle-core" % monocleVersion % "test",
"com.github.julien-truffaut" %% "monocle-macro" % monocleVersion % "test",
"org.scalaz" %% "testz-core" % testzVersion % "test",
"org.scalaz" %% "testz-stdlib" % testzVersion % "test",
"org.scalaz" %% "testz-runner" % testzVersion % "test"
).map(_.exclude("org.scalaz", "scalaz"))
)
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ trait SchemaModule {

def prim[A](prim: Prim[A]): Schema[A] = Schema.PrimSchema(prim)

def record[A](field: Schema.Field[A, _], fields: Schema.Field[A, _]*): Schema[A] =
Schema.RecordSchema(NonEmptyList.nels(field, fields: _*))
def record[A](fields: FreeAp[Schema.Field[A, ?], A]): Schema[A] =
Schema.RecordSchema(fields)

def union[A](branch: Schema.Branch[A, _], branches: Schema.Branch[A, _]*): Schema[A] =
Schema.Union(NonEmptyList.nels(branch, branches: _*))
Expand All @@ -24,15 +24,19 @@ trait SchemaModule {
base: Schema[A0],
getter: Getter[A, A0],
default: Option[A0]
): Schema.Field[A, A0] =
Schema.Field.Essential(id, base, getter, default)
): FreeAp[Schema.Field[A, ?], A0] =
FreeAp.lift[Schema.Field[A, ?], A0](
Schema.Field.Essential[A, A0](id, base, getter, default)
)

def nonEssentialField[A, A0](
id: ProductTermId,
base: Schema[A0],
getter: monocle.Optional[A, A0]
): Schema.Field[A, Option[A0]] =
Schema.Field.NonEssential(id, base, getter)
): FreeAp[Schema.Field[A, ?], Option[A0]] =
FreeAp.lift[Schema.Field[A, ?], Option[A0]](
Schema.Field.NonEssential(id, base, getter)
)

def branch[A, A0](id: SumTermId, base: Schema[A0], prism: Prism[A, A0]): Schema.Branch[A, A0] =
Schema.Branch(id, base, prism)
Expand All @@ -42,10 +46,10 @@ trait SchemaModule {
object Schema {
// Writing final here triggers a warning, using sealed instead achieves almost the same effect
// without warning. See https://issues.scala-lang.org/browse/SI-4440
sealed case class PrimSchema[A](prim: Prim[A]) extends Schema[A]
sealed case class Union[A](terms: NonEmptyList[Branch[A, _]]) extends Schema[A]
sealed case class RecordSchema[A](terms: NonEmptyList[Field[A, _]]) extends Schema[A]
sealed case class SeqSchema[A](element: Schema[A]) extends Schema[List[A]]
sealed case class PrimSchema[A](prim: Prim[A]) extends Schema[A]
sealed case class Union[A](terms: NonEmptyList[Branch[A, _]]) extends Schema[A]
sealed case class RecordSchema[A](fields: FreeAp[Field[A, ?], A]) extends Schema[A]
sealed case class SeqSchema[A](element: Schema[A]) extends Schema[List[A]]

/**
* A term of type `A0` in a sum of type `A`.
Expand Down
File renamed without changes.
24 changes: 24 additions & 0 deletions modules/core/src/test/scala/Main.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package scalaz

package schema

import testz._
import testz.runner._

import scala.concurrent.Await
import scala.concurrent.ExecutionContext.global
import scala.concurrent.duration.Duration

object Main extends TestMain {

def tests[T](harness: Harness[T]): List[(String, T)] =
List(
("Examples", SchemaModuleExamples.tests(harness))
)

def main(args: Array[String]): Unit = {
val result = Await.result(Runner(suites(harness), global), Duration.Inf)

if (result.failed) throw new Exception("some tests failed")
}
}
123 changes: 123 additions & 0 deletions modules/core/src/test/scala/SchemaModuleExamples.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package scalaz

package schema

import testz._

import scalaz.Scalaz._

object SchemaModuleExamples {

val jsonModule = new SchemaModule {
type Prim[A] = JsonSchema.Prim[A]
type ProductTermId = String
type SumTermId = String
}

def tests[T](harness: Harness[T]): T = {
import harness._
import jsonModule._
import jsonModule.Schema._

section("Manipulating Schemas")(
test("Building Schemas using the smart constructors") { () =>
val personSchema = record[Person](
^(
essentialField[Person, String](
"name",
prim(JsonSchema.JsonString),
Person.name,
None
),
nonEssentialField[Person, Role](
"role",
union[Role](
branch(
"user",
record[User](
essentialField(
"active",
prim(JsonSchema.JsonBool),
Person.active,
None
).map(User.apply)
),
Person.user
),
branch(
"admin",
record[Admin](
essentialField(
"rights",
seq(prim(JsonSchema.JsonString)),
Person.rights,
None
).map(Admin.apply)
),
Person.admin
)
),
Person.role
)
)(Person.apply)
)
val expected =
RecordSchema[Person](
^(
FreeAp.lift[Field[Person, ?], String](
Field.Essential(
"name",
PrimSchema(JsonSchema.JsonString),
Person.name,
None
)
),
FreeAp.lift[Field[Person, ?], Option[Role]](
Field.NonEssential(
"role",
Union(
NonEmptyList.nels[Branch[Role, _]](
Branch[Role, User](
"user",
RecordSchema[User](
FreeAp
.lift[Field[User, ?], Boolean](
Field.Essential(
"active",
PrimSchema(JsonSchema.JsonBool),
Person.active,
None
)
)
.map(User)
),
Person.user
),
Branch[Role, Admin](
"admin",
RecordSchema[Admin](
FreeAp
.lift[Field[Admin, ?], List[String]](
Field.Essential(
"rights",
SeqSchema(PrimSchema(JsonSchema.JsonString)),
Person.rights,
None
)
)
.map(Admin)
),
Person.admin
)
)
),
Person.role
)
)
)(Person.apply)
)
assert(personSchema == expected)
}
)
}
}
3 changes: 0 additions & 3 deletions modules/scalacheck/build.sbt

This file was deleted.

66 changes: 65 additions & 1 deletion modules/scalacheck/src/main/scala/GenModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,70 @@ package schema

package scalacheck

import org.scalacheck.Gen

trait GenModule extends SchemaModule {
// def gen[A](schema: Schema[A])(implicit arb: Arbitrary[Prim[_]]): Gen[A]

trait ToGen[S[_]] {
def toGen: S ~> Gen
}

implicit class ToGenOps[A](schema: Schema[A]) {

def toGen(implicit primToGen: Prim ~> Gen): Gen[A] =
schemaToGen(primToGen)(schema)
}

private def schemaToGen(implicit primToGen: Prim ~> Gen): Schema ~> Gen =
new (Schema ~> Gen) {
override def apply[A](schema: Schema[A]): Gen[A] = schema match {
case prim: Schema.PrimSchema[_] => primToGen(prim.prim)
case record: Schema.RecordSchema[_] => recordGen(record)
case union: Schema.Union[_] => unionGen(union)
case seq: Schema.SeqSchema[_] => seqGen(seq)
}
}

private def recordGen[A](
schema: Schema.RecordSchema[A]
)(implicit
primToGen: Prim ~> Gen
): Gen[A] = {
implicit val genAp: Applicative[Gen] = new Applicative[Gen] {
override def ap[T, U](fa: => Gen[T])(f: => Gen[T => U]): Gen[U] =
fa.flatMap(a => f.map(_(a)))
override def point[T](a: => T): Gen[T] = Gen.const(a)
}

schema.fields.foldMap(new (Schema.Field[A, ?] ~> Gen) {
override def apply[B](fa: Schema.Field[A, B]): Gen[B] = fa match {
case Schema.Field.Essential(_, base, _, _) =>
schemaToGen(primToGen)(base)
case Schema.Field.NonEssential(_, base, _) =>
Gen.option(schemaToGen(primToGen)(base))
}
})
}

private def unionGen[A](schema: Schema.Union[A])(implicit primToGen: Prim ~> Gen): Gen[A] = {
val branchGens = schema.terms.map(term => branchGen(term))
branchGens.tail.headOption
.fold(branchGens.head)(
g => Gen.oneOf(branchGens.head, g, branchGens.tail.toList.tail: _*)
)
}

private def branchGen[A, A0](
branch: Schema.Branch[A, A0]
)(implicit
primToGen: Prim ~> Gen
): Gen[A] =
schemaToGen(primToGen)(branch.base).map(branch.prism.reverseGet)

private def seqGen[A](
schema: Schema.SeqSchema[A]
)(implicit
primToGen: Prim ~> Gen
): Gen[List[A]] =
Gen.listOf(schemaToGen(primToGen)(schema.element))
}