Skip to content

Commit cfcea52

Browse files
authored
Merge pull request #23 from rolang/add-json-type-support
add any/object to json type mapping support
2 parents 34ed707 + 5dd4421 commit cfcea52

7 files changed

Lines changed: 200 additions & 64 deletions

File tree

build.sbt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,15 @@ lazy val noPublish = Seq(
4242

4343
val sttpClient4Version = "4.0.13"
4444

45-
val zioVersion = "2.1.22"
45+
val zioVersion = "2.1.23"
4646

47-
val zioJsonVersion = "0.7.45"
47+
val zioJsonVersion = "0.8.0"
4848

49-
// NOTE: update from 2.38.3 to 2.38.4 causes compilation error with some recursive types on codec derivation
50-
val jsoniterVersion = "2.38.2"
49+
val jsoniterVersion = "2.38.6"
5150

5251
val munitVersion = "1.2.1"
5352

54-
val upickleVersion = "4.4.1"
53+
val upickleVersion = "4.4.2"
5554

5655
addCommandAlias("fmt", "all scalafmtSbt scalafmt test:scalafmt")
5756

@@ -152,7 +151,8 @@ lazy val testProjects: CompositeProject = new CompositeProject {
152151
arrayType
153152
),
154153
libraryDependencies ++= Seq(
155-
"org.scalameta" %% "munit" % munitVersion % Test
154+
"org.scalameta" %% "munit" % munitVersion % Test,
155+
"com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % jsoniterVersion % Test
156156
) ++ dependencyByConfig(httpSource = httpSource, jsonCodec = jsonCodec, arrayType = arrayType)
157157
)
158158
}

docker-compose.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
services:
22
pubsub:
33
# https://console.cloud.google.com/gcr/images/google.com:cloudsdktool/GLOBAL/cloud-sdk
4-
image: gcr.io/google.com/cloudsdktool/cloud-sdk:498.0.0-emulators
4+
image: gcr.io/google.com/cloudsdktool/cloud-sdk:542.0.0-emulators
55
ports:
66
- "8085:8085"
77
command: gcloud beta emulators pubsub start --project=any --host-port=0.0.0.0:8085

modules/cli/src/main/scala/cli.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//> using jvm system
33
//> using scala 3.7.4
44
//> using file ../../../../core/shared/src/main/scala/codegen.scala
5-
//> using dep com.lihaoyi::upickle:4.4.1
5+
//> using dep com.lihaoyi::upickle:4.4.2
66

77
package gcp.codegen.cli
88

modules/core/shared/src/main/scala/codegen.scala

Lines changed: 119 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,9 @@ def generateBySpec(
116116
" def apply(",
117117
specs.queryParameters
118118
.map((k, v) =>
119-
s"""${{ toComment(v.description) }} ${toScalaName(k)}: ${v.typ
119+
s"""${{ toComment(v.description) }} ${toScalaName(k)}: ${v.typ
120120
.withOptional(true)
121-
.scalaType(config.arrayType)} = None"""
121+
.scalaType(config.arrayType, config.jsonCodec)} = None"""
122122
)
123123
.mkString(" ", ",\n ", ""),
124124
"): QueryParameters =",
@@ -211,7 +211,8 @@ def generateBySpec(
211211
httpSource = config.httpSource,
212212
hasProps = p => specs.hasProps(p),
213213
arrType = config.arrayType,
214-
commonQueryParams = specs.queryParameters
214+
commonQueryParams = specs.queryParameters,
215+
jsonCodec = config.jsonCodec
215216
)
216217
val path = resourceKey.dirPath(resourcesPath) / s"$resourceName.scala"
217218
Files.writeString(path, code)
@@ -220,7 +221,7 @@ def generateBySpec(
220221
},
221222
// generate schemas with properties
222223
for {
223-
commonCodecs <- Future {
224+
(commonCodecs, hasExtraCodecs) <- Future {
224225
commonSchemaCodecs(
225226
schemas = specs.schemas.filter(_._2.properties.nonEmpty),
226227
pkg = schemasPkg,
@@ -229,10 +230,10 @@ def generateBySpec(
229230
hasProps = p => specs.hasProps(p),
230231
arrType = config.arrayType
231232
) match
232-
case None => Nil
233-
case Some(codecs) =>
234-
Files.writeString(commonCodecsPath, codecs)
235-
List(commonCodecsPath.toFile())
233+
case None => (Nil, false)
234+
case Some((content, hasExtraCodecs)) =>
235+
Files.writeString(commonCodecsPath, content)
236+
(List(commonCodecsPath.toFile()), hasExtraCodecs)
236237
}
237238
schemas <- Future
238239
.traverse(specs.schemas) { (schemaPath, schema) =>
@@ -246,7 +247,7 @@ def generateBySpec(
246247
hasProps = p => specs.hasProps(p),
247248
arrType = config.arrayType,
248249
commonCodecsPkg =
249-
if commonCodecs.nonEmpty && schema.hasArrays then Some(commonCodecsPkg) else None
250+
if commonCodecs.nonEmpty && hasExtraCodecs then Some(commonCodecsPkg) else None
250251
)
251252
else
252253
// create a type alias for objects without properties
@@ -286,7 +287,8 @@ def resourceCode(
286287
httpSource: HttpSource,
287288
arrType: ArrayType,
288289
hasProps: SchemaPath => Boolean,
289-
commonQueryParams: Map[String, Parameter]
290+
commonQueryParams: Map[String, Parameter],
291+
jsonCodec: JsonCodec
290292
) =
291293
val sttpClientPkg = httpSource match
292294
case HttpSource.Sttp4 => "sttp.client4"
@@ -334,11 +336,13 @@ def resourceCode(
334336

335337
val (requiredParams, optParams) = method.scalaParameters.partition(_._2.required)
336338
def params(indent: String) =
337-
requiredParams.map((n, t) => s"${toComment(t.description, indent)}$indent$n: ${t.scalaType(arrType)}") :::
338-
req.toList.map(r => s"${indent}request: ${r.scalaType(arrType)}") :::
339+
requiredParams.map((n, t) =>
340+
s"${toComment(t.description, indent)}$indent$n: ${t.scalaType(arrType, jsonCodec)}"
341+
) :::
342+
req.toList.map(r => s"${indent}request: ${r.scalaType(arrType, jsonCodec)}") :::
339343
uploadProtocol.toList.map((typ, default) => s"${indent}uploadProtocol: $typ = \"$default\"") :::
340344
optParams.map((n, t) =>
341-
s"${toComment(t.description, indent)}$indent$n: ${t.scalaType(arrType)} = None"
345+
s"${toComment(t.description, indent)}$indent$n: ${t.scalaType(arrType, jsonCodec)} = None"
342346
) :::
343347
List(
344348
s"${indent}endpointUrl: $sttpUriPkg = $rootPkg.baseUrl",
@@ -388,7 +392,7 @@ def resourceCode(
388392

389393
val (resType, mapResponse) = method.response match
390394
case Some(r) if r.schemaPath.forall(hasProps) =>
391-
val bodyType = r.scalaType(arrType)
395+
val bodyType = r.scalaType(arrType, jsonCodec)
392396

393397
(
394398
responseType(bodyType),
@@ -455,7 +459,8 @@ def schemasCode(
455459
if jsonCodec == JsonCodec.ZioJson then SchemaType.EnumType.Literal
456460
else SchemaType.EnumType.Nominal(s"$scalaName.${toScalaTypeName(n)}")
457461
s"${toComment(t.withTypeDescription)} ${toScalaName(n)}: ${
458-
(if (t.optional) s"${t.scalaType(arrType, enumType)} = None" else t.scalaType(arrType, enumType))
462+
(if (t.optional) s"${t.scalaType(arrType, jsonCodec, enumType)} = None"
463+
else t.scalaType(arrType, jsonCodec, enumType))
459464
}"
460465
}
461466
.mkString("", ",\n", "")}
@@ -487,8 +492,38 @@ def commonSchemaCodecs(
487492
jsonCodec: JsonCodec,
488493
hasProps: SchemaPath => Boolean,
489494
arrType: ArrayType
490-
): Option[String] = {
491-
(jsonCodec, arrType) match
495+
): Option[(String, Boolean)] = {
496+
(jsonCodec match
497+
case JsonCodec.ZioJson => Nil
498+
case JsonCodec.Jsoniter =>
499+
List(
500+
s"""|package $pkg
501+
|
502+
|import com.github.plokhotnyuk.jsoniter_scala.core.*
503+
|import com.github.plokhotnyuk.jsoniter_scala.macros.*
504+
|
505+
|opaque type Json = Array[Byte]
506+
|
507+
|object Json {
508+
|
509+
| given codec: JsonValueCodec[Json] = new JsonValueCodec[Json] {
510+
| override def decodeValue(in: JsonReader, default: Json): Json = in.readRawValAsBytes()
511+
|
512+
| override def encodeValue(x: Json, out: JsonWriter): Unit = out.writeRawVal(x)
513+
|
514+
| override val nullValue: Json = new Array[Byte](0)
515+
| }
516+
|
517+
| extension (v: Json)
518+
| def readAsUnsafe[T: JsonValueCodec]: T = readFromArray(v)
519+
| def readAs[T: JsonValueCodec]: Either[Throwable, T] =
520+
| try
521+
| Right(readFromArray(v))
522+
| catch
523+
| case t: Throwable => Left(t)
524+
|}""".stripMargin -> false
525+
)
526+
).appendedAll((jsonCodec, arrType) match
492527
case (JsonCodec.Jsoniter, ArrayType.ZioChunk) =>
493528
schemas.toList
494529
.flatMap((sk, sv) =>
@@ -497,41 +532,54 @@ def commonSchemaCodecs(
497532
val enumType =
498533
if jsonCodec == JsonCodec.ZioJson then SchemaType.EnumType.Literal
499534
else SchemaType.EnumType.Nominal(s"${sk.lastOption.getOrElse("")}.${toScalaTypeName(k)}")
500-
typ.scalaType(arrType, enumType)
535+
typ.scalaType(arrType, jsonCodec, enumType)
501536
}
502537
)
503538
.distinct match
504-
case Nil => None
539+
case Nil => Nil
505540
case props =>
506-
Some(
541+
List(
507542
List(
508-
s"""|package $pkg
509-
|
510-
|import com.github.plokhotnyuk.jsoniter_scala.core.*
511-
|import com.github.plokhotnyuk.jsoniter_scala.macros.*
512-
|import zio.Chunk""".stripMargin,
513543
"",
514544
s"object $objName {",
545+
"",
546+
// to ensure codec for Chunk[Json] is added since it may not be present in props
547+
"""| given JsonChunkCodec: JsonValueCodec[zio.Chunk[Json]] = new JsonValueCodec[zio.Chunk[Json]] {
548+
| val arrCodec: JsonValueCodec[Array[Json]] = JsonCodecMaker.make
549+
|
550+
| override val nullValue: zio.Chunk[Json] = zio.Chunk.empty
551+
|
552+
| override def decodeValue(in: JsonReader, default: zio.Chunk[Json]): zio.Chunk[Json] =
553+
| zio.Chunk.fromArray(arrCodec.decodeValue(in, default.toArray))
554+
|
555+
| override def encodeValue(x: zio.Chunk[Json], out: JsonWriter): Unit =
556+
| arrCodec.encodeValue(x.toArray, out)
557+
| }""".stripMargin,
558+
"",
515559
props
560+
.filterNot(_ == "Json") // to void duplicate codec for Chunk[Json]
516561
.map { t =>
517562
val prefix = " given " + toScalaName(t + "ChunkCodec")
518-
s"""|${prefix}: JsonValueCodec[Chunk[$t]] = new JsonValueCodec[Chunk[$t]] {
519-
| val arrCodec: JsonValueCodec[Array[$t]] = JsonCodecMaker.make
520-
|
521-
| override val nullValue: Chunk[$t] = Chunk.empty
522-
|
523-
| override def decodeValue(in: JsonReader, default: Chunk[$t]): Chunk[$t] =
524-
| Chunk.fromArray(arrCodec.decodeValue(in, default.toArray))
525-
|
526-
| override def encodeValue(x: Chunk[$t], out: JsonWriter): Unit =
527-
| arrCodec.encodeValue(x.toArray, out)
528-
|}""".stripMargin
563+
s"""|${prefix}: JsonValueCodec[zio.Chunk[$t]] = new JsonValueCodec[zio.Chunk[$t]] {
564+
| val arrCodec: JsonValueCodec[Array[$t]] = JsonCodecMaker.make
565+
|
566+
| override val nullValue: zio.Chunk[$t] = zio.Chunk.empty
567+
|
568+
| override def decodeValue(in: JsonReader, default: zio.Chunk[$t]): zio.Chunk[$t] =
569+
| zio.Chunk.fromArray(arrCodec.decodeValue(in, default.toArray))
570+
|
571+
| override def encodeValue(x: zio.Chunk[$t], out: JsonWriter): Unit =
572+
| arrCodec.encodeValue(x.toArray, out)
573+
|}""".stripMargin
529574
}
530575
.mkString("\n\n"),
531576
"}"
532-
).mkString("\n")
577+
).mkString("\n") -> true
533578
)
534-
case _ => None
579+
case _ => Nil) match
580+
case Nil => None
581+
case codecs => Some((codecs.map(_._1).mkString("\n"), codecs.exists(_._2)))
582+
535583
}
536584

537585
case class FlatPath(path: String, params: List[String])
@@ -655,7 +703,8 @@ case class Parameter(
655703
required: Boolean = false,
656704
pattern: Option[String] = None
657705
) {
658-
def scalaType(arrType: ArrayType): String = typ.withOptional(!required).scalaType(arrType)
706+
def scalaType(arrType: ArrayType, jsonCodec: JsonCodec): String =
707+
typ.withOptional(!required).scalaType(arrType, jsonCodec)
659708
}
660709

661710
object Parameter:
@@ -672,8 +721,8 @@ object Parameter:
672721

673722
case class Property(description: Option[String], typ: SchemaType, readOnly: Boolean = false) {
674723
def optional: Boolean = typ.optional || readOnly
675-
def scalaType(arrType: ArrayType, enumType: SchemaType.EnumType): String =
676-
typ.withOptional(optional).scalaType(arrType, enumType)
724+
def scalaType(arrType: ArrayType, jsonCodec: JsonCodec, enumType: SchemaType.EnumType): String =
725+
typ.withOptional(optional).scalaType(arrType, jsonCodec, enumType)
677726
def schemaPath: Option[SchemaPath] = typ.schemaPath
678727
def nestedSchemaPath: Option[SchemaPath] = typ.schemaPath.filter(_.hasNested)
679728

@@ -696,10 +745,11 @@ object Property:
696745
enum SchemaType(val optional: Boolean):
697746
case Ref(ref: SchemaPath, override val optional: Boolean) extends SchemaType(optional)
698747
case Primitive(
699-
`type`: String,
748+
`type`: "string" | "integer" | "number" | "boolean",
700749
override val optional: Boolean,
701750
format: Option[String] = None
702751
) extends SchemaType(optional)
752+
case Any(override val optional: Boolean) extends SchemaType(optional)
703753
case Array(items: SchemaType, override val optional: Boolean) extends SchemaType(optional)
704754
case Object(additionalProperties: SchemaType, override val optional: Boolean) extends SchemaType(optional)
705755
case Enum(typ: String, values: List[SchemaType.EnumValue], override val optional: Boolean) extends SchemaType(true)
@@ -724,9 +774,11 @@ enum SchemaType(val optional: Boolean):
724774
case t: Array => t.copy(optional = o)
725775
case t: Object => t.copy(optional = o)
726776
case t: Enum => t.copy(optional = o)
777+
case t: Any => t.copy(optional = o)
727778

728779
def scalaType(
729780
arrayType: ArrayType,
781+
jsonCodec: JsonCodec,
730782
enumType: SchemaType.EnumType = SchemaType.EnumType.Literal
731783
): String = this match
732784
case Primitive("string", _, Some("google-datetime")) => toType("java.time.OffsetDateTime")
@@ -736,13 +788,24 @@ enum SchemaType(val optional: Boolean):
736788
case Primitive("number", _, Some("double" | "float")) => toType("Double")
737789
case Primitive("boolean", _, _) => toType("Boolean")
738790
case Ref(ref, _) => toType(ref.scalaName)
739-
case Array(t, _) => toType(arrayType.toScalaType(t.scalaType(arrayType, enumType)))
740-
case Object(t, _) => toType(s"Map[String, ${t.scalaType(arrayType)}]")
791+
case Array(t, _) => toType(arrayType.toScalaType(t.scalaType(arrayType, jsonCodec, enumType)))
792+
case Object(t: Primitive, _) => toType(s"Map[String, ${t.scalaType(arrayType, jsonCodec)}]")
793+
case _: Object =>
794+
toType(
795+
jsonCodec match
796+
case JsonCodec.ZioJson => "zio.json.ast.Json.Obj"
797+
case JsonCodec.Jsoniter => "Json" // assuming the codecs package is imported
798+
)
741799
case Enum(_, values, _) =>
742800
enumType match
743801
case SchemaType.EnumType.Literal => toType(values.map(v => v.value).mkString("\"", "\" | \"", "\""))
744802
case SchemaType.EnumType.Nominal(name) => toType(name)
745-
case _ => toType("String")
803+
case _ =>
804+
toType(
805+
jsonCodec match
806+
case JsonCodec.ZioJson => "zio.json.ast.Json"
807+
case JsonCodec.Jsoniter => "Json" // assuming the codecs package is imported
808+
)
746809

747810
object SchemaType:
748811
case class EnumValue(value: String, enumDescription: String)
@@ -751,11 +814,15 @@ object SchemaType:
751814
case Literal
752815
case Nominal(prefix: String)
753816

754-
private def toPrimitive(o: ujson.Obj, optional: Boolean) = SchemaType.Primitive(
755-
`type` = o("type").str,
756-
optional = optional,
757-
format = o.value.get("format").map(_.str)
758-
)
817+
private def toPrimitiveOrAny(o: ujson.Obj, optional: Boolean) =
818+
o("type").str match
819+
case typ: ("string" | "integer" | "number" | "boolean") =>
820+
SchemaType.Primitive(
821+
`type` = typ,
822+
optional = optional,
823+
format = o.value.get("format").map(_.str)
824+
)
825+
case _ => Any(optional)
759826

760827
def readType(context: SchemaPath, o: ujson.Obj): SchemaType =
761828
val desc = o.value.get("description").map(_.str)
@@ -794,9 +861,9 @@ object SchemaType:
794861
.map(_.group(1))
795862
.toList
796863
.collect { case v: String => EnumValue(value = v, enumDescription = "") } match
797-
case Nil => toPrimitive(o, optional)
864+
case Nil => toPrimitiveOrAny(o, optional)
798865
case values => SchemaType.Enum(typ = o("type").str, optional = optional, values = values)
799-
else toPrimitive(o, optional)
866+
else toPrimitiveOrAny(o, optional)
800867
else SchemaType.Ref(context, optional)
801868

802869
opaque type SchemaPath = Vector[String]

0 commit comments

Comments
 (0)