diff --git a/argus/src/main/scala/argus/macros/FromSchema.scala b/argus/src/main/scala/argus/macros/FromSchema.scala index a7298be..1697a67 100644 --- a/argus/src/main/scala/argus/macros/FromSchema.scala +++ b/argus/src/main/scala/argus/macros/FromSchema.scala @@ -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 } @@ -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 } @@ -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 } @@ -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" => { @@ -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] ) } @@ -163,12 +171,28 @@ 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 @@ -176,6 +200,7 @@ class SchemaMacros(val c: Context) { class enum extends scala.annotation.StaticAnnotation class union extends scala.annotation.StaticAnnotation ..$rawSchemaDef + ..$runtimeDefs ..$defs ..${ mkCodecs(params.jsonEnd, defs, tname.toString :: Nil) } } diff --git a/argus/src/test/scala/argus/macros/FromSchemaSpec.scala b/argus/src/test/scala/argus/macros/FromSchemaSpec.scala index f043393..c85fa5c 100644 --- a/argus/src/test/scala/argus/macros/FromSchemaSpec.scala +++ b/argus/src/test/scala/argus/macros/FromSchemaSpec.scala @@ -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._ @@ -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 diff --git a/build.sbt b/build.sbt index 51c2eae..a25746b 100644 --- a/build.sbt +++ b/build.sbt @@ -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: _*) diff --git a/runtime/src/main/scala/io/circe/argus/HasSchemaSource.scala b/runtime/src/main/scala/io/circe/argus/HasSchemaSource.scala new file mode 100644 index 0000000..a254b91 --- /dev/null +++ b/runtime/src/main/scala/io/circe/argus/HasSchemaSource.scala @@ -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 + } +}