From 719fb1549a0ad93edd231b17acc45e904e667946 Mon Sep 17 00:00:00 2001 From: Jules Ivanic Date: Sat, 24 Aug 2024 04:31:46 +1000 Subject: [PATCH] Fix `Map` representation in generated OpenAPI specs (#3049) --- .../zio/http/gen/openapi/EndpointGen.scala | 12 +++++----- .../endpoint/openapi/OpenAPIGenSpec.scala | 11 ++++++---- .../http/endpoint/openapi/JsonSchema.scala | 22 ++++++++++++++++++- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/zio-http-gen/src/main/scala/zio/http/gen/openapi/EndpointGen.scala b/zio-http-gen/src/main/scala/zio/http/gen/openapi/EndpointGen.scala index 1de39e8bf6..8b760d7254 100644 --- a/zio-http-gen/src/main/scala/zio/http/gen/openapi/EndpointGen.scala +++ b/zio-http-gen/src/main/scala/zio/http/gen/openapi/EndpointGen.scala @@ -1007,11 +1007,9 @@ final case class EndpointGen(config: Config) { case JsonSchema.ArrayType(None, _, _) => None case JsonSchema.ArrayType(Some(schema), _, _) => schemaToCode(schema, openAPI, name, annotations) - case JsonSchema.Object(properties, additionalProperties, _) - if properties.nonEmpty && additionalProperties.isRight => - // Can't be an object and a map at the same time + case obj: JsonSchema.Object if obj.isInvalid => throw new Exception("Object with properties and additionalProperties is not supported") - case obj @ JsonSchema.Object(properties, additionalProperties, _) if additionalProperties.isLeft => + case obj @ JsonSchema.Object(properties, _, _) if obj.isClosedDictionary => val unvalidatedFields = fieldsOfObject(openAPI, annotations)(obj) val fields = validateFields(unvalidatedFields) val nested = @@ -1044,10 +1042,10 @@ final case class EndpointGen(config: Config) { enums = Nil, ), ) - case JsonSchema.Object(_, _, _) => - // properties.isEmpty && additionalProperties.isRight + case JsonSchema.Object(_, _, _) => + // if obt.isOpenDictionary throw new IllegalArgumentException("Top-level maps are not supported") - case JsonSchema.Enum(enums) => + case JsonSchema.Enum(enums) => Some( Code.File( List("component", name.capitalize + ".scala"), diff --git a/zio-http/jvm/src/test/scala/zio/http/endpoint/openapi/OpenAPIGenSpec.scala b/zio-http/jvm/src/test/scala/zio/http/endpoint/openapi/OpenAPIGenSpec.scala index 9fdc294750..967bd1630d 100644 --- a/zio-http/jvm/src/test/scala/zio/http/endpoint/openapi/OpenAPIGenSpec.scala +++ b/zio-http/jvm/src/test/scala/zio/http/endpoint/openapi/OpenAPIGenSpec.scala @@ -2812,6 +2812,13 @@ object OpenAPIGenSpec extends ZIOSpecDefault { | "nestedOption" : { | "$ref" : "#/components/schemas/Recursive" | }, + | "nestedMap" : { + | "type" : "object", + | "properties" : {}, + | "additionalProperties" : { + | "$ref" : "#/components/schemas/Recursive" + | } + | }, | "nestedList" : { | "type" : | "array", @@ -2823,10 +2830,6 @@ object OpenAPIGenSpec extends ZIOSpecDefault { | "$ref" : "#/components/schemas/NestedRecursive" | } | }, - | "additionalProperties" : - | { - | "$ref" : "#/components/schemas/Recursive" - | }, | "required" : [ | "nestedOption", | "nestedList", diff --git a/zio-http/shared/src/main/scala/zio/http/endpoint/openapi/JsonSchema.scala b/zio-http/shared/src/main/scala/zio/http/endpoint/openapi/JsonSchema.scala index 0d8c5dacbe..ed8e1bc284 100644 --- a/zio-http/shared/src/main/scala/zio/http/endpoint/openapi/JsonSchema.scala +++ b/zio-http/shared/src/main/scala/zio/http/endpoint/openapi/JsonSchema.scala @@ -1212,10 +1212,30 @@ object JsonSchema { additionalProperties: Either[Boolean, JsonSchema], required: Chunk[java.lang.String], ) extends JsonSchema { + + /** + * This Object represents an "open dictionary", aka a Map + * + * See: https://github.com/zio/zio-http/issues/3048#issuecomment-2306291192 + */ + def isOpenDictionary: Boolean = properties.isEmpty && additionalProperties.isRight + + /** + * This Object represents a "closed dictionary", aka a case class + * + * See: https://github.com/zio/zio-http/issues/3048#issuecomment-2306291192 + */ + def isClosedDictionary: Boolean = additionalProperties.isLeft + + /** + * Can't represent a case class and a Map at the same time + */ + def isInvalid: Boolean = properties.nonEmpty && additionalProperties.isRight + def addAll(value: Chunk[(java.lang.String, JsonSchema)]): Object = value.foldLeft(this) { case (obj, (name, schema)) => schema match { - case Object(properties, additionalProperties, required) => + case thatObj @ Object(properties, additionalProperties, required) if thatObj.isClosedDictionary => obj.copy( properties = obj.properties ++ properties, additionalProperties = combineAdditionalProperties(obj.additionalProperties, additionalProperties),