@@ -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
537585case 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
661710object Parameter :
@@ -672,8 +721,8 @@ object Parameter:
672721
673722case 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:
696745enum 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
747810object 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
802869opaque type SchemaPath = Vector [String ]
0 commit comments