Skip to content

Commit

Permalink
Experimental abstraction over Argus-generated types
Browse files Browse the repository at this point in the history
  • Loading branch information
travisbrown committed Nov 10, 2020
1 parent 9046e53 commit a62ec68
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 10 deletions.
41 changes: 33 additions & 8 deletions argus/src/main/scala/argus/macros/FromSchema.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ object JsonEngs {
* to the output file (defaults to None, so no package name is written).
* @param name The name used for the root case class that is generated. Defaults to "Root"
* @param rawSchema Includes the raw schema string in the companion object
* @param runtime Produces code for abstracting over Argus-generated types
*/
@compileTimeOnly("You must enable the macro paradise plugin.")
class fromSchemaJson(json: String, debug: Boolean = false, jsonEng: Option[JsonEng] = None, outPath: Option[String] = None,
outPathPackage: Option[String] = None, name: String = "Root", rawSchema: Boolean = false) extends StaticAnnotation {
outPathPackage: Option[String] = None, name: String = "Root", rawSchema: Boolean = false,
runtime: Boolean = false) extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro SchemaMacros.fromSchemaMacroImpl
}

Expand All @@ -43,10 +45,12 @@ class fromSchemaJson(json: String, debug: Boolean = false, jsonEng: Option[JsonE
* to the output file (defaults to None, so no package name is written).
* @param name The name used for the root case class that is generated. Defaults to "Root"
* @param rawSchema Includes the raw schema string in the companion object
* @param runtime Produces code for abstracting over Argus-generated types
*/
@compileTimeOnly("You must enable the macro paradise plugin.")
class fromSchemaResource(path: String, debug: Boolean = false, jsonEng: Option[JsonEng] = None, outPath: Option[String] = None,
outPathPackage: Option[String] = None, name: String = "Root", rawSchema: Boolean = false) extends StaticAnnotation {
outPathPackage: Option[String] = None, name: String = "Root", rawSchema: Boolean = false,
runtime: Boolean = false) extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro SchemaMacros.fromSchemaMacroImpl
}

Expand All @@ -62,10 +66,12 @@ class fromSchemaResource(path: String, debug: Boolean = false, jsonEng: Option[J
* to the output file (defaults to None, so no package name is written).
* @param name The name used for the root case class that is generated. Defaults to "Root"
* @param rawSchema Includes the raw schema string in the companion object
* @param runtime Produces code for abstracting over Argus-generated types
*/
@compileTimeOnly("You must enable the macro paradise plugin.")
class fromSchemaURL(url: String, debug: Boolean = false, jsonEng: Option[JsonEng] = None, outPath: Option[String],
outPathPackage: Option[String] = None, name: String = "Root", rawSchema: Boolean = false) extends StaticAnnotation {
outPathPackage: Option[String] = None, name: String = "Root", rawSchema: Boolean = false,
runtime: Boolean = false) extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro SchemaMacros.fromSchemaMacroImpl
}

Expand All @@ -79,14 +85,15 @@ class SchemaMacros(val c: Context) {
import helpers._

case class Params(schema: Schema.Root, debug: Boolean, jsonEnd: Option[JsonEng], outPath: Option[String],
outPathPackage: Option[String], name: String, rawSchema: Option[String])
outPathPackage: Option[String], name: String, rawSchema: Option[String],
runtime: Boolean = false)

private def extractParams(prefix: Tree): Params = {
val q"new $name (..$paramASTs)" = prefix
val (Ident(TypeName(fn: String))) = name

val commonParams = ("debug", false) :: ("jsonEng", q"Some(JsonEngs.Circe)") :: ("outPath", None) ::
("outPathPackage", None) :: ("name", "Root") :: ("rawSchema", false) :: Nil
("outPathPackage", None) :: ("name", "Root") :: ("rawSchema", false) :: ("runtime", false) :: Nil

val (params, schemaString)= fn match {
case "fromSchemaResource" => {
Expand Down Expand Up @@ -118,7 +125,8 @@ class SchemaMacros(val c: Context) {
params("outPath").asInstanceOf[Option[String]],
params("outPathPackage").asInstanceOf[Option[String]],
params("name").asInstanceOf[String],
rawSchema
rawSchema,
params("runtime").asInstanceOf[Boolean]
)
}

Expand Down Expand Up @@ -163,19 +171,36 @@ class SchemaMacros(val c: Context) {
// Add definitions and codecs to annotated object
case (objDef @ q"$mods object $tname extends { ..$earlydefns } with ..$parents { $self => ..$stats }") :: _ => {

val (_, defs) = modelBuilder.mkSchemaDef(params.name, schema)
val (rootTpe, defs) = modelBuilder.mkSchemaDef(params.name, schema)

val rawSchemaDef = rawSchema.map { s =>
q"""val rawSchema: String = $s"""
q"""val schemaSource: String = $s"""
}.toList

val runtimeDefs: List[Tree] = (
if (params.runtime && !rootTpe.isEmpty) {
rawSchema.map { s =>
val name = TypeName(params.name)
val hasSchemaSourceInstanceName = TermName(params.name + "HasSchemaSource")

q"""
implicit val $hasSchemaSourceInstanceName: _root_.io.circe.argus.HasSchemaSource[$name] =
_root_.io.circe.argus.HasSchemaSource.instance[$name]($s)
"""
}
} else {
None
}
).toList

q"""
$mods object $tname extends { ..$earlydefns } with ..$parents { $self =>
..$stats

class enum extends scala.annotation.StaticAnnotation
class union extends scala.annotation.StaticAnnotation
..$rawSchemaDef
..$runtimeDefs
..$defs
..${ mkCodecs(params.jsonEnd, defs, tname.toString :: Nil) }
}
Expand Down
27 changes: 26 additions & 1 deletion argus/src/test/scala/argus/macros/FromSchemaSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import java.io.File
import java.time.ZonedDateTime
import java.util.UUID

import io.circe.argus.HasSchemaSource
import io.circe.argus.json.JsonDiff
import io.circe.argus.schema.Schema
import cats.syntax.either._
Expand Down Expand Up @@ -612,8 +613,32 @@ class FromSchemaSpec extends AnyFlatSpec with Matchers with JsonMatchers {
""", rawSchema = true)
object Foo

Foo.rawSchema should === (expected)
Foo.schemaSource should === (expected)
}

it should "generate HasSchemaSource instances" in {
val expected = """
{
"type": "object",
"properties": {
"name": { "type" : "string" }
}
}
""".filter(_ != '\n')

@fromSchemaJson("""
{
"type": "object",
"properties": {
"name": { "type" : "string" }
}
}
""", rawSchema = true, runtime = true)
object Foo

HasSchemaSource[Foo.Root].value should === (expected)
}

"Complex example" should "work end to end" in {
@fromSchemaResource("/vega-lite-schema.json")
object Vega
Expand Down
7 changes: 6 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,14 @@ lazy val argus = project
"org.scalatest" %% "scalatest" % Vers.scalatest % Test
)
)
.dependsOn(runtime % Test)

lazy val runtime = project
.settings(moduleName := "circe-argus-runtime")
.settings(commonSettings: _*)

lazy val root = (project in file("."))
.aggregate(argus)
.aggregate(argus, runtime)
.settings(commonSettings: _*)
.settings(noPublishSettings: _*)

Expand Down
16 changes: 16 additions & 0 deletions runtime/src/main/scala/io/circe/argus/HasSchemaSource.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.circe.argus

/**
* Supports abstraction over Argus-generated types that have a schema string associated with them.
*/
trait HasSchemaSource[A] {
def value: String
}

object HasSchemaSource {
def apply[A](implicit instance: HasSchemaSource[A]): HasSchemaSource[A] = instance

def instance[A](source: String): HasSchemaSource[A] = new HasSchemaSource[A] {
def value: String = source
}
}

0 comments on commit a62ec68

Please sign in to comment.