Skip to content

Commit

Permalink
[gen] adding support for "Any" and "AnyObject" as Json in codegen
Browse files Browse the repository at this point in the history
logic properly handles correctly these cases:

  extra_attributes:
    type: object
    additionalProperties: true

  extra_attributes:
    type: object
    additionalProperties: {}

  extra_attributes:
    additionalProperties: true

but fails for this case, which might not be an issue (or an issue with zio-schema):
  extra_attributes:
    additionalProperties: {}

Also, small code re-arranges to avoid:
"Exhaustivity analysis reached max recursion depth, not all missing cases are reported."
compile error due to too many cases in pattern match.
  • Loading branch information
hochgi committed Dec 25, 2024
1 parent 2846f13 commit b81551c
Show file tree
Hide file tree
Showing 6 changed files with 255 additions and 165 deletions.
114 changes: 44 additions & 70 deletions zio-http-gen/src/main/scala/zio/http/gen/openapi/EndpointGen.scala
Original file line number Diff line number Diff line change
Expand Up @@ -673,85 +673,44 @@ final case class EndpointGen(config: Config) {
} else None

@tailrec
private def schemaToPathCodec(schema: JsonSchema, openAPI: OpenAPI, name: String): Code.PathSegmentCode = {
private def schemaToPathCodec(schema: JsonSchema, openAPI: OpenAPI, name: String): Code.PathSegmentCode =
schema match {
case JsonSchema.AnnotatedSchema(s, _) => schemaToPathCodec(s, openAPI, name)
case JsonSchema.RefSchema(ref) => schemaToPathCodec(resolveSchemaRef(openAPI, ref), openAPI, name)
case JsonSchema.Integer(JsonSchema.IntegerFormat.Int32, _, _, _, _, _) =>
Code.PathSegmentCode(name = name, segmentType = Code.CodecType.Int)
case JsonSchema.Integer(JsonSchema.IntegerFormat.Int64, _, _, _, _, _) =>
Code.PathSegmentCode(name = name, segmentType = Code.CodecType.Long)
case JsonSchema.Integer(JsonSchema.IntegerFormat.Timestamp, _, _, _, _, _) =>
Code.PathSegmentCode(name = name, segmentType = Code.CodecType.Long)
case JsonSchema.String(Some(JsonSchema.StringFormat.UUID), _, _, _) =>
Code.PathSegmentCode(name = name, segmentType = Code.CodecType.UUID)
case JsonSchema.String(Some(JsonSchema.StringFormat.Date), _, _, _) =>
Code.PathSegmentCode(name = name, segmentType = Code.CodecType.LocalDate)
case JsonSchema.String(Some(JsonSchema.StringFormat.DateTime), _, _, _) =>
Code.PathSegmentCode(name = name, segmentType = Code.CodecType.Instant)
case JsonSchema.String(Some(JsonSchema.StringFormat.Time), _, _, _) =>
Code.PathSegmentCode(name = name, segmentType = Code.CodecType.LocalTime)
case JsonSchema.String(Some(JsonSchema.StringFormat.Duration), _, _, _) =>
Code.PathSegmentCode(name = name, segmentType = Code.CodecType.Duration)
case JsonSchema.String(_, _, _, _) =>
Code.PathSegmentCode(name = name, segmentType = Code.CodecType.String)
case JsonSchema.Boolean =>
Code.PathSegmentCode(name = name, segmentType = Code.CodecType.Boolean)
case JsonSchema.OneOfSchema(_) => throw new Exception("Alternative path variables are not supported")
case JsonSchema.AllOfSchema(_) => throw new Exception("Path variables must have exactly one schema")
case JsonSchema.AnyOfSchema(_) => throw new Exception("Path variables must have exactly one schema")
case JsonSchema.Number(_, _, _, _, _, _) =>
throw new Exception("Floating point path variables are currently not supported")
case JsonSchema.ArrayType(_, _, _) => throw new Exception("Array path variables are not supported")
case JsonSchema.Object(_, _, _) => throw new Exception("Object path variables are not supported")
case JsonSchema.Enum(_) => throw new Exception("Enum path variables are not supported")
case JsonSchema.Null => throw new Exception("Null path variables are not supported")
case JsonSchema.AnyJson => throw new Exception("AnyJson path variables are not supported")
case s: JsonSchema.Integer => Code.PathSegmentCode(name = name, segmentType = integerCodec(s.format))
case s: JsonSchema.String => Code.PathSegmentCode(name = name, segmentType = stringCodec(s.format))
case JsonSchema.Boolean => Code.PathSegmentCode(name = name, segmentType = Code.CodecType.Boolean)
case JsonSchema.OneOfSchema(_) => throw new Exception("Alternative path variables are not supported")
case JsonSchema.AllOfSchema(_) => throw new Exception("Path variables must have exactly one schema")
case JsonSchema.AnyOfSchema(_) => throw new Exception("Path variables must have exactly one schema")
case JsonSchema.ArrayType(_, _, _) => throw new Exception("Array path variables are not supported")
case JsonSchema.Object(_, _, _) => throw new Exception("Object path variables are not supported")
case JsonSchema.Enum(_) => throw new Exception("Enum path variables are not supported")
case JsonSchema.Null => throw new Exception("Null path variables are not supported")
case JsonSchema.AnyJson => throw new Exception("AnyJson path variables are not supported")
case _: JsonSchema.Number => throw new Exception("Floating point path variables are not supported")
}
}

@tailrec
private def schemaToQueryParamCodec(
schema: JsonSchema,
openAPI: OpenAPI,
name: String,
): Code.QueryParamCode = {
schema match {
case JsonSchema.AnnotatedSchema(s, _) =>
schemaToQueryParamCodec(s, openAPI, name)
case JsonSchema.RefSchema(ref) =>
schemaToQueryParamCodec(resolveSchemaRef(openAPI, ref), openAPI, name)
case JsonSchema.Integer(JsonSchema.IntegerFormat.Int32, _, _, _, _, _) =>
Code.QueryParamCode(name = name, queryType = Code.CodecType.Int)
case JsonSchema.Integer(JsonSchema.IntegerFormat.Int64, _, _, _, _, _) =>
Code.QueryParamCode(name = name, queryType = Code.CodecType.Long)
case JsonSchema.Integer(JsonSchema.IntegerFormat.Timestamp, _, _, _, _, _) =>
Code.QueryParamCode(name = name, queryType = Code.CodecType.Long)
case JsonSchema.String(Some(JsonSchema.StringFormat.Date), _, _, _) =>
Code.QueryParamCode(name = name, queryType = Code.CodecType.LocalDate)
case JsonSchema.String(Some(JsonSchema.StringFormat.DateTime), _, _, _) =>
Code.QueryParamCode(name = name, queryType = Code.CodecType.Instant)
case JsonSchema.String(Some(JsonSchema.StringFormat.Duration), _, _, _) =>
Code.QueryParamCode(name = name, queryType = Code.CodecType.Duration)
case JsonSchema.String(Some(JsonSchema.StringFormat.Time), _, _, _) =>
Code.QueryParamCode(name = name, queryType = Code.CodecType.LocalTime)
case JsonSchema.String(Some(JsonSchema.StringFormat.UUID), _, _, _) =>
Code.QueryParamCode(name = name, queryType = Code.CodecType.UUID)
case JsonSchema.String(_, _, _, _) =>
Code.QueryParamCode(name = name, queryType = Code.CodecType.String)
case JsonSchema.Boolean =>
Code.QueryParamCode(name = name, queryType = Code.CodecType.Boolean)
case JsonSchema.OneOfSchema(_) => throw new Exception("Alternative query parameters are not supported")
case JsonSchema.AllOfSchema(_) => throw new Exception("Query parameters must have exactly one schema")
case JsonSchema.AnyOfSchema(_) => throw new Exception("Query parameters must have exactly one schema")
case JsonSchema.Number(_, _, _, _, _, _) =>
throw new Exception("Floating point query parameters are currently not supported")
case JsonSchema.ArrayType(_, _, _) => throw new Exception("Array query parameters are not supported")
case JsonSchema.Object(_, _, _) => throw new Exception("Object query parameters are not supported")
case JsonSchema.Enum(_) => throw new Exception("Enum query parameters are not supported")
case JsonSchema.Null => throw new Exception("Null query parameters are not supported")
case JsonSchema.AnyJson => throw new Exception("AnyJson query parameters are not supported")
}
): Code.QueryParamCode = schema match {
case JsonSchema.AnnotatedSchema(s, _) => schemaToQueryParamCodec(s, openAPI, name)
case JsonSchema.RefSchema(ref) => schemaToQueryParamCodec(resolveSchemaRef(openAPI, ref), openAPI, name)
case JsonSchema.Boolean => Code.QueryParamCode(name = name, queryType = Code.CodecType.Boolean)
case s: JsonSchema.Integer => Code.QueryParamCode(name = name, queryType = integerCodec(s.format))
case s: JsonSchema.String => Code.QueryParamCode(name = name, queryType = stringCodec(s.format))
case _: JsonSchema.Number => throw new Exception("Floating point query parameters are not supported")
case JsonSchema.OneOfSchema(_) => throw new Exception("Alternative query parameters are not supported")
case JsonSchema.AllOfSchema(_) => throw new Exception("Query parameters must have exactly one schema")
case JsonSchema.AnyOfSchema(_) => throw new Exception("Query parameters must have exactly one schema")
case JsonSchema.ArrayType(_, _, _) => throw new Exception("Array query parameters are not supported")
case JsonSchema.Object(_, _, _) => throw new Exception("Object query parameters are not supported")
case JsonSchema.Enum(_) => throw new Exception("Enum query parameters are not supported")
case JsonSchema.Null => throw new Exception("Null query parameters are not supported")
case JsonSchema.AnyJson => throw new Exception("AnyJson query parameters are not supported")
}

private def fieldsOfObject(openAPI: OpenAPI, annotations: Chunk[JsonSchema.MetaData])(
Expand Down Expand Up @@ -1042,6 +1001,7 @@ final case class EndpointGen(config: Config) {
properties.map { case (name, schema) => name -> schema.withoutAnnotations }.collect {
case (name, schema)
if !schema.isInstanceOf[JsonSchema.RefSchema]
&& !(schema == JsonSchema.AnyJson)
&& !schema.isPrimitive
&& !schema.isCollection =>
schemaToCode(schema, openAPI, name.capitalize, Chunk.empty)
Expand Down Expand Up @@ -1093,7 +1053,7 @@ final case class EndpointGen(config: Config) {
),
)
case JsonSchema.Null => throw new Exception("Null query parameters are not supported")
case JsonSchema.AnyJson => throw new Exception("AnyJson query parameters are not supported")
case JsonSchema.AnyJson => None
}
}

Expand Down Expand Up @@ -1378,4 +1338,18 @@ final case class EndpointGen(config: Config) {
}
}

private def integerCodec(format: JsonSchema.IntegerFormat): Code.CodecType = format match {
case JsonSchema.IntegerFormat.Int32 => Code.CodecType.Int
case JsonSchema.IntegerFormat.Int64 => Code.CodecType.Long
case JsonSchema.IntegerFormat.Timestamp => Code.CodecType.Long
}

private def stringCodec(format: Option[JsonSchema.StringFormat]): Code.CodecType = format match {
case Some(JsonSchema.StringFormat.Date) => Code.CodecType.LocalDate
case Some(JsonSchema.StringFormat.DateTime) => Code.CodecType.Instant
case Some(JsonSchema.StringFormat.Duration) => Code.CodecType.Duration
case Some(JsonSchema.StringFormat.Time) => Code.CodecType.LocalTime
case Some(JsonSchema.StringFormat.UUID) => Code.CodecType.UUID
case _ => Code.CodecType.String
}
}
5 changes: 3 additions & 2 deletions zio-http-gen/src/main/scala/zio/http/gen/scala/CodeGen.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import java.nio.charset.StandardCharsets
import java.nio.file.StandardOpenOption._
import java.nio.file._

import scala.util.matching.Regex

object CodeGen {

private val EndpointImports =
Expand Down Expand Up @@ -265,6 +263,9 @@ object CodeGen {
case Code.TypeRef(name) =>
Nil -> name

case Code.ScalaType.JsonAST =>
List(Code.Import("zio.json.ast.Json")) -> "Json"

case scalaType =>
throw new Exception(s"Unknown ScalaType: $scalaType")
}
Expand Down
14 changes: 14 additions & 0 deletions zio-http-gen/src/test/resources/AnimalWithAny.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package test.component

import zio.json.ast.Json
import zio.schema._
import zio.schema.annotation.fieldName

case class Animal(
name: String,
eats: Json,
@fieldName("extra_attributes") extraAttributes: Map[String, Json],
)
object Animal {
implicit val codec: Schema[Animal] = DeriveSchema.gen[Animal]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
info:
title: Animals Service
version: 0.0.1
tags:
- name: Animals_API
paths:
/api/v1/zoo/{animal}:
get:
operationId: get_animal
parameters:
- in: path
name: animal
schema:
type: string
required: true
tags:
- Animals_API
description: Get animals by species name
responses:
"200":
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Animal'
description: OK
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/HttpError'
description: Internal Server Error
openapi: 3.0.3
components:
schemas:
Animal:
type: object
required:
- name
- eats
- extra_attributes
properties:
name:
type: string
eats: {}
extra_attributes:
type: object
additionalProperties: true
24 changes: 24 additions & 0 deletions zio-http-gen/src/test/scala/zio/http/gen/scala/CodeGenSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1012,6 +1012,30 @@ object CodeGenSpec extends ZIOSpecDefault {
}
}
} @@ TestAspect.exceptScala3,
test("Schema with any and any object") {
val openAPIString = stringFromResource("/inline_schema_any_and_any_object.yaml")

openApiFromYamlString(openAPIString) { oapi =>
codeGenFromOpenAPI(
oapi,
Config.default.copy(
fieldNamesNormalization = Config.default.fieldNamesNormalization.copy(enableAutomatic = true),
),
) { testDir =>
allFilesShouldBe(
testDir.toFile,
List(
"api/v1/zoo/Animal.scala",
"component/Animal.scala",
),
) && fileShouldBe(
testDir,
"component/Animal.scala",
"/AnimalWithAny.scala",
)
}
}
} @@ TestAspect.exceptScala3,
test("Generate all responses") {
val oapi =
OpenAPI(
Expand Down
Loading

0 comments on commit b81551c

Please sign in to comment.