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
48 changes: 38 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ The generator can be used with any tool that can perform system calls to a comma
See example under [example/generate.scala](./example/generate.scala).

```scala
//> using scala 3.7.3
//> using dep dev.rolang::gcp-codegen::0.0.8
//> using scala 3.7.4
//> using dep dev.rolang::gcp-codegen::0.0.12

import gcp.codegen.*, java.nio.file.*, GeneratorConfig.*

Expand Down Expand Up @@ -55,13 +55,41 @@ See output in `example/out`.

| Configuration | Description | Options | Default |
| ------------------- | ---------------- | ------- | --- |
| -specs | Can be `stdin` or a path to the JSON file. | | |
| -out-dir | Ouput directory | | |
| -out-pkg | Output package | | |
| -http-source | Generated http source. | [Sttp4](https://sttp.softwaremill.com/en/stable) | |
| -json-codec | Generated JSON codec | [Jsoniter](https://github.com/plokhotnyuk/jsoniter-scala), [ZioJson](https://zio.dev/zio-json) | |
| -array-type | Collection type for JSON arrays | `List`, `Vector`, `Array`, `ZioChunk` | `List` |
| -include-resources | Optional resource filter. | | |
| -specs | Can be `stdin` or a path to the JSON file. | | |
| -out-dir | Output directory | | |
| -out-pkg | Output package | | |
| -http-source | Generated http source. | [Sttp4](https://sttp.softwaremill.com/en/stable) | |
| -json-codec | Generated JSON codec | [Jsoniter](https://github.com/plokhotnyuk/jsoniter-scala), [ZioJson](https://zio.dev/zio-json) | |
| -jsoniter-json-type | In case of Jsoniter a fully qualified name of the custom type that can represent a raw Json value | |
| -array-type | Collection type for JSON arrays | `List`, `Vector`, `Array`, `ZioChunk` | `List` |
| -include-resources | Optional resource filter. | | |

##### Jsoniter Json type and codec example
Jsoniter doesn't ship with a type that can represent raw Json values to be used for mapping of `any` / `object` types,
but it provides methods to read / write raw values as bytes (related [issue](https://github.com/plokhotnyuk/jsoniter-scala/issues/1257)).
Given that we can create a custom type with a codec which can look for example like [that](modules/example-jsoniter-json/shared/src/main/scala/json.scala):
```scala
package example.jsoniter
import com.github.plokhotnyuk.jsoniter_scala.core.*

opaque type Json = Array[Byte]
object Json:
def writeToJson[T: JsonValueCodec](v: T): Json = writeToArray[T](v)

given codec: JsonValueCodec[Json] = new JsonValueCodec[Json]:
override def decodeValue(in: JsonReader, default: Json): Json = in.readRawValAsBytes()
override def encodeValue(x: Json, out: JsonWriter): Unit = out.writeRawVal(x)
override val nullValue: Json = Array[Byte](0)

extension (v: Json)
def readAsUnsafe[T: JsonValueCodec]: T = readFromArray(v)
def readAs[T: JsonValueCodec]: Either[Throwable, T] =
try Right(readFromArray(v))
catch case t: Throwable => Left(t)
```
Then pass it as argument to the code generator like `-jsoniter-json-type=_root_.example.jsoniter.Json`.
Since this type and codec can be shared across generated clients it has to be provided (at least for now)
instead of being generated for each client to avoid duplicated / redundant code.

##### Examples:

Expand All @@ -79,7 +107,7 @@ curl 'https://pubsub.googleapis.com/$discovery/rest?version=v1' > pubsub_v1.json
-specs=./pubsub_v1.json \
-out-pkg=gcp.pubsub.v1 \
-http-source=sttp4 \
-json-codec=jsoniter \
-json-codec=ziojson \
-include-resources='projects.*,!projects.snapshots' # optional filters
```

Expand Down
30 changes: 27 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ val zioVersion = "2.1.23"

val zioJsonVersion = "0.8.0"

val jsoniterVersion = "2.38.6"
val jsoniterVersion = "2.38.8"

val munitVersion = "1.2.1"

Expand All @@ -58,13 +58,16 @@ lazy val root = (project in file("."))
.aggregate(
core.native,
core.jvm,
exampleJsoniterJson.native,
exampleJsoniterJson.jvm,
cli
)
.aggregate(testProjects.componentProjects.map(p => LocalProject(p.id)) *)
.settings(noPublish)

// for supporting code inspection / testing of generated code via test_gen.sh script
lazy val testLocal = (project in file("test-local"))
.dependsOn(exampleJsoniterJson.jvm)
.settings(
libraryDependencies ++= Seq(
"com.softwaremill.sttp.client4" %% "core" % sttpClient4Version,
Expand Down Expand Up @@ -101,6 +104,20 @@ lazy val cli = project
nativeConfig := nativeConfig.value.withMultithreading(false)
)

lazy val exampleJsoniterJson = crossProject(JVMPlatform, NativePlatform)
.in(file("modules/example-jsoniter-json"))
.settings(noPublish)
.settings(
name := "example-jsoniter-json",
moduleName := "example-jsoniter-json"
)
.settings(
libraryDependencies ++= Seq(
"com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % jsoniterVersion,
"com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % jsoniterVersion % "compile-internal"
)
)

def dependencyByConfig(httpSource: String, jsonCodec: String, arrayType: String): Seq[ModuleID] = {
(httpSource match {
case "Sttp4" => Seq("com.softwaremill.sttp.client4" %% "core" % sttpClient4Version)
Expand Down Expand Up @@ -136,7 +153,7 @@ lazy val testProjects: CompositeProject = new CompositeProject {
arrayType <- Seq("ZioChunk", "List")
id = s"test-$apiName-$apiVersion-${httpSource}-${jsonCodec}-${arrayType}".toLowerCase()
} yield {
Project
val p = Project
.apply(
id = id,
base = file("modules") / id
Expand All @@ -155,6 +172,12 @@ lazy val testProjects: CompositeProject = new CompositeProject {
"com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % jsoniterVersion % Test
) ++ dependencyByConfig(httpSource = httpSource, jsonCodec = jsonCodec, arrayType = arrayType)
)

if (jsonCodec == "Jsoniter") {
p.dependsOn(exampleJsoniterJson.componentProjects.map(p => ClasspathDependency(p, p.configuration)) *)
} else {
p
}
}
}

Expand Down Expand Up @@ -235,7 +258,8 @@ def codegenTask(
s"-out-pkg=$basePkgName",
s"-http-source=$httpSource",
s"-json-codec=$jsonCodec",
s"-array-type=$arrayType"
s"-array-type=$arrayType",
s"-jsoniter-json-type=_root_.example.jsoniter.Json"
).mkString(" ") ! ProcessLogger(l => logger.info(l), e => errs += e)) match {
case 0 => ()
case c => throw new InterruptedException(s"Failure on code generation: ${errs.mkString("\n")}")
Expand Down
6 changes: 3 additions & 3 deletions example/generate.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//> using scala 3.7.3
//> using dep dev.rolang::gcp-codegen::0.0.8
//> using scala 3.7.4
//> using dep dev.rolang::gcp-codegen::0.0.12

import gcp.codegen.*, java.nio.file.*, GeneratorConfig.*

Expand All @@ -10,7 +10,7 @@ import gcp.codegen.*, java.nio.file.*, GeneratorConfig.*
outDir = Path.of("out"),
outPkg = "example.pubsub.v1",
httpSource = HttpSource.Sttp4,
jsonCodec = JsonCodec.Jsoniter,
jsonCodec = JsonCodec.ZioJson,
arrayType = ArrayType.List,
preprocess = specs => specs
)
Expand Down
19 changes: 13 additions & 6 deletions modules/cli/src/main/scala/cli.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ import scala.concurrent.duration.*

private def argsToTask(args: Seq[String]): Either[String, Task] =
val argsMap = args.toList
.flatMap(_.split('=').map(_.trim().toLowerCase()))
.flatMap(_.split('=').map(_.trim()))
.sliding(2, 2)
.collect { case a :: b :: _ =>
a -> b
a.toLowerCase() -> b
}
.toMap

Expand All @@ -57,10 +57,17 @@ private def argsToTask(args: Seq[String]): Either[String, Task] =
.get("-http-source")
.flatMap(v => HttpSource.values.find(_.toString().equalsIgnoreCase(v)))
.toRight("Missing or invalid -http-source")
jsonCodec <- argsMap
.get("-json-codec")
.flatMap(v => JsonCodec.values.find(_.toString().equalsIgnoreCase(v)))
.toRight("Missing or invalid -json-codec")
jsonCodec <- (
argsMap
.get("-json-codec")
.map(_.toLowerCase()),
argsMap.get("-jsoniter-json-type")
) match {
case (Some("ziojson"), _) => Right(JsonCodec.ZioJson)
case (Some("jsoniter"), Some(jsonType)) => Right(JsonCodec.Jsoniter(jsonType))
case (Some("jsoniter"), None) => Left("Missing -jsoniter-json-type")
case _ => Left("Missing or invalid -json-codec")
}
arrayType <- argsMap.get("-array-type") match
case None => Right(ArrayType.List)
case Some(v) => ArrayType.values.find(_.toString().equalsIgnoreCase(v)).toRight(s"Invalid array-type $v")
Expand Down
Loading