diff --git a/rest/.jvm/src/test/resources/RestTestApi.json b/rest/.jvm/src/test/resources/RestTestApi.json index 24c0b689c..633379a2f 100644 --- a/rest/.jvm/src/test/resources/RestTestApi.json +++ b/rest/.jvm/src/test/resources/RestTestApi.json @@ -805,17 +805,20 @@ "oneOf": [ { "type": "object", + "title": "RestEntity", "properties": { "RestEntity": { "$ref": "#/components/schemas/RestEntity" } }, + "additionalProperties": false, "required": [ "RestEntity" ] }, { "type": "object", + "title": "RestOtherEntity", "properties": { "RestOtherEntity": { "type": "object", @@ -830,23 +833,28 @@ } } }, + "additionalProperties": false, "required": [ "fuu", "kek" ] } }, + "additionalProperties": false, "required": [ "RestOtherEntity" ] }, { "type": "object", + "title": "SingletonEntity", "properties": { "SingletonEntity": { - "type": "object" + "type": "object", + "additionalProperties": false } }, + "additionalProperties": false, "required": [ "SingletonEntity" ] diff --git a/rest/src/main/scala/io/udash/rest/openapi/RestStructure.scala b/rest/src/main/scala/io/udash/rest/openapi/RestStructure.scala index a850aed62..7cc64bdfc 100644 --- a/rest/src/main/scala/io/udash/rest/openapi/RestStructure.scala +++ b/rest/src/main/scala/io/udash/rest/openapi/RestStructure.scala @@ -40,11 +40,20 @@ object RestStructure extends AdtMetadataCompanion[RestStructure] { val caseSchemas = cases.map { c => val baseSchema = resolver.resolve(c.caseSchema(caseFieldOpt)) if (caseFieldOpt.nonEmpty) baseSchema - else RefOr(Schema( - `type` = DataType.Object, - properties = IListMap(c.info.rawName -> baseSchema), - required = List(c.info.rawName) - )) + else { + val adjustedBase = baseSchema match { + case RefOr.Value(s) if s.`type`.contains(DataType.Object) && s.additionalProperties == AdditionalProperties.Flag(value = true) => + RefOr(s.copy(additionalProperties = AdditionalProperties.Flag(value = false))) + case other => other + } + RefOr(Schema( + `type` = DataType.Object, + title = c.info.rawName, + properties = IListMap(c.info.rawName -> adjustedBase), + additionalProperties = AdditionalProperties.Flag(value = false), + required = List(c.info.rawName) + )) + } } val disc = caseFieldOpt.map { caseFieldName => val mapping = IListMap((cases zip caseSchemas).collect { diff --git a/rest/src/test/scala/io/udash/rest/openapi/RestSchemaTest.scala b/rest/src/test/scala/io/udash/rest/openapi/RestSchemaTest.scala index 183591cfd..b98cee7d7 100644 --- a/rest/src/test/scala/io/udash/rest/openapi/RestSchemaTest.scala +++ b/rest/src/test/scala/io/udash/rest/openapi/RestSchemaTest.scala @@ -63,6 +63,18 @@ object HierarchyRoot { RestStructure.materialize[HierarchyRoot[String]].standaloneSchema.named("StringHierarchy") } +sealed trait NestedHierarchy +final case class NestedHierarchyCase(value: String) extends NestedHierarchy +final case class NestedHierarchyCase2(value: Int) extends NestedHierarchy + +object NestedHierarchy extends RestDataCompanion[NestedHierarchy] + +sealed trait NestedHierarchyWithEdgeCases +@transparent final case class TransparentCase(value: String) extends NestedHierarchyWithEdgeCases +final case class MapCase(entries: Map[String, Int]) extends NestedHierarchyWithEdgeCases + +object NestedHierarchyWithEdgeCases extends RestDataCompanion[NestedHierarchyWithEdgeCases] + @flatten("case") sealed trait FullyQualifiedHierarchy object FullyQualifiedHierarchy extends RestDataCompanionWithDeps[FullyQualifiedNames.type, FullyQualifiedHierarchy] { final case class Foo(str: String) extends FullyQualifiedHierarchy @@ -349,6 +361,114 @@ class RestSchemaTest extends AnyFunSuite { |}""".stripMargin) } + test("Nested hierarchy") { + assert(allSchemasStr[NestedHierarchy] == + """{ + | "NestedHierarchy": { + | "type": "object", + | "oneOf": [ + | { + | "type": "object", + | "title": "NestedHierarchyCase", + | "properties": { + | "NestedHierarchyCase": { + | "type": "object", + | "properties": { + | "value": { + | "type": "string" + | } + | }, + | "additionalProperties": false, + | "required": [ + | "value" + | ] + | } + | }, + | "additionalProperties": false, + | "required": [ + | "NestedHierarchyCase" + | ] + | }, + | { + | "type": "object", + | "title": "NestedHierarchyCase2", + | "properties": { + | "NestedHierarchyCase2": { + | "type": "object", + | "properties": { + | "value": { + | "type": "integer", + | "format": "int32" + | } + | }, + | "additionalProperties": false, + | "required": [ + | "value" + | ] + | } + | }, + | "additionalProperties": false, + | "required": [ + | "NestedHierarchyCase2" + | ] + | } + | ] + | } + |}""".stripMargin + ) + } + + test("Nested hierarchy with transparent and map cases") { + assert(allSchemasStr[NestedHierarchyWithEdgeCases] == + """{ + | "NestedHierarchyWithEdgeCases": { + | "type": "object", + | "oneOf": [ + | { + | "type": "object", + | "title": "TransparentCase", + | "properties": { + | "TransparentCase": { + | "type": "string" + | } + | }, + | "additionalProperties": false, + | "required": [ + | "TransparentCase" + | ] + | }, + | { + | "type": "object", + | "title": "MapCase", + | "properties": { + | "MapCase": { + | "type": "object", + | "properties": { + | "entries": { + | "type": "object", + | "additionalProperties": { + | "type": "integer", + | "format": "int32" + | } + | } + | }, + | "additionalProperties": false, + | "required": [ + | "entries" + | ] + | } + | }, + | "additionalProperties": false, + | "required": [ + | "MapCase" + | ] + | } + | ] + | } + |}""".stripMargin + ) + } + test("Customized schema name") { assert(allSchemasStr[CustomSchemaNameHierarchy] == """{