diff --git a/parser/src/commonMain/kotlin/io/github/nomisrev/openapi/Schema.kt b/parser/src/commonMain/kotlin/io/github/nomisrev/openapi/Schema.kt index 94191d8..86760e5 100644 --- a/parser/src/commonMain/kotlin/io/github/nomisrev/openapi/Schema.kt +++ b/parser/src/commonMain/kotlin/io/github/nomisrev/openapi/Schema.kt @@ -43,10 +43,7 @@ public data class Schema( val deprecated: Boolean? = null, val maxProperties: Int? = null, val minProperties: Int? = null, - /** - * Unlike JSON Schema this value MUST conform to the defined type for this parameter. Note: is - * ignored for required parameters. - */ + /** Unlike JSON Schema this value MUST conform to the defined type for this parameter. */ val default: DefaultValue? = null, val type: Type? = null, val format: String? = null, diff --git a/typed/src/commonMain/kotlin/io/github/nomisrev/openapi/Constraints.kt b/typed/src/commonMain/kotlin/io/github/nomisrev/openapi/Constraints.kt new file mode 100644 index 0000000..6061a87 --- /dev/null +++ b/typed/src/commonMain/kotlin/io/github/nomisrev/openapi/Constraints.kt @@ -0,0 +1,42 @@ +package io.github.nomisrev.openapi + +data class NumberConstraint( + val exclusiveMinimum: Boolean, + val minimum: Double, + val exclusiveMaximum: Boolean, + val maximum: Double, + val multipleOf: Double? +) { + constructor(schema: Schema) : this( + schema.exclusiveMinimum ?: false, + schema.minimum ?: Double.NEGATIVE_INFINITY, + schema.exclusiveMaximum ?: false, + schema.maximum ?: Double.POSITIVE_INFINITY, + schema.multipleOf + ) +} + +data class TextConstraint(val maxLength: Int, val minLength: Int, val pattern: String?) { + constructor(schema: Schema) : this(schema.maxLength ?: Int.MAX_VALUE, schema.minLength ?: 0, schema.pattern) +} + +data class CollectionConstraint( + val minItems: Int, + val maxItems: Int, +) { + constructor(schema: Schema) : this( + schema.minItems ?: 0, + schema.maxItems ?: Int.MAX_VALUE, + ) +} + +// TODO `not` is not supported yet +data class ObjectConstraint( + val minProperties: Int, + val maxProperties: Int, +) { + constructor(schema: Schema): this( + schema.minProperties ?: 0, + schema.maxProperties ?: Int.MAX_VALUE, + ) +} diff --git a/typed/src/commonMain/kotlin/io/github/nomisrev/openapi/Model.kt b/typed/src/commonMain/kotlin/io/github/nomisrev/openapi/Model.kt index d5ba5d0..5fe12a2 100644 --- a/typed/src/commonMain/kotlin/io/github/nomisrev/openapi/Model.kt +++ b/typed/src/commonMain/kotlin/io/github/nomisrev/openapi/Model.kt @@ -129,16 +129,26 @@ sealed interface Model { val description: String? sealed interface Primitive : Model { - data class Int(val default: kotlin.Int?, override val description: kotlin.String?) : Primitive - - data class Double(val default: kotlin.Double?, override val description: kotlin.String?) : - Primitive + data class Int( + val default: kotlin.Int?, + override val description: kotlin.String?, + val bounds: NumberConstraint + ) : Primitive + + data class Double( + val default: kotlin.Double?, + override val description: kotlin.String?, + val bounds: NumberConstraint + ) : Primitive data class Boolean(val default: kotlin.Boolean?, override val description: kotlin.String?) : Primitive - data class String(val default: kotlin.String?, override val description: kotlin.String?) : - Primitive + data class String( + val default: kotlin.String?, + override val description: kotlin.String?, + val bounds: TextConstraint + ) : Primitive data class Unit(override val description: kotlin.String?) : Primitive @@ -150,6 +160,8 @@ sealed interface Model { is String -> default?.let { "\"$it\"" } is Unit -> null } + + companion object } data class OctetStream(override val description: String?) : Model @@ -158,32 +170,40 @@ sealed interface Model { sealed interface Collection : Model { val inner: Model + val bounds: CollectionConstraint data class List( override val inner: Model, val default: kotlin.collections.List?, - override val description: String? + override val description: String?, + override val bounds: CollectionConstraint ) : Collection data class Set( override val inner: Model, val default: kotlin.collections.List?, - override val description: String? + override val description: String?, + override val bounds: CollectionConstraint ) : Collection - data class Map(override val inner: Model, override val description: String?) : Collection { - val key = Primitive.String(null, null) + data class Map( + override val inner: Model, + override val description: String?, + override val bounds: CollectionConstraint + ) : Collection { + val key = Primitive.String(null, null, TextConstraint(Int.MAX_VALUE, 0, null)) } + + companion object } - @Serializable data class Object( val context: NamingContext, override val description: String?, val properties: List, - val inline: List + val inline: List, + val constraint: ObjectConstraint? ) : Model { - @Serializable data class Property( val baseName: String, val model: Model, @@ -195,6 +215,8 @@ sealed interface Model { val isNullable: Boolean, val description: String? ) + + companion object } data class Union( @@ -228,4 +250,6 @@ sealed interface Model { override val description: String? ) : Enum } + + companion object } diff --git a/typed/src/commonMain/kotlin/io/github/nomisrev/openapi/OpenAPITransformer.kt b/typed/src/commonMain/kotlin/io/github/nomisrev/openapi/OpenAPITransformer.kt index 030fc40..f4df02b 100644 --- a/typed/src/commonMain/kotlin/io/github/nomisrev/openapi/OpenAPITransformer.kt +++ b/typed/src/commonMain/kotlin/io/github/nomisrev/openapi/OpenAPITransformer.kt @@ -200,15 +200,21 @@ private class OpenAPITransformer(private val openAPI: OpenAPI) { Type.Basic.Array -> collection(context) Type.Basic.Boolean -> Primitive.Boolean(default("Boolean", String::toBooleanStrictOrNull), description) - Type.Basic.Integer -> Primitive.Int(default("Integer", String::toIntOrNull), description) + Type.Basic.Integer -> + Primitive.Int(default("Integer", String::toIntOrNull), description, NumberConstraint(this)) Type.Basic.Number -> - Primitive.Double(default("Number", String::toDoubleOrNull), description) + Primitive.Double( + default("Number", String::toDoubleOrNull), + description, + NumberConstraint(this) + ) Type.Basic.String -> if (format == "binary") Model.OctetStream(description) else Primitive.String( default("String", String::toString) { it.joinToString() }, - description + description, + TextConstraint(this) ) Type.Basic.Object -> toObject(context) Type.Basic.Null -> TODO("Schema.Type.Basic.Null") @@ -398,7 +404,8 @@ private class OpenAPITransformer(private val openAPI: OpenAPI) { is Resolved.Value -> NamingContext.Nested(Named(name), context) } nestedModel(resolved, pContext) - } + }, + ObjectConstraint(this) ) } @@ -429,8 +436,9 @@ private class OpenAPITransformer(private val openAPI: OpenAPI) { } null -> null } - return if (uniqueItems == true) Collection.Set(inner.value, default, description) - else Collection.List(inner.value, default, description) + return if (uniqueItems == true) + Collection.Set(inner.value, default, description, CollectionConstraint(this)) + else Collection.List(inner.value, default, description, CollectionConstraint(this)) } fun Schema.toEnum(context: NamingContext, enums: List): Enum.Closed { @@ -673,7 +681,10 @@ private class OpenAPITransformer(private val openAPI: OpenAPI) { response.isEmpty() -> Pair( statusCode, - Route.ReturnType(Primitive.String(null, response.description), response.extensions) + Route.ReturnType( + Primitive.String(null, response.description, TextConstraint(Int.MAX_VALUE, 0, null)), + response.extensions + ) ) else -> throw IllegalStateException("OpenAPI requires at least 1 valid response. $response") diff --git a/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/ConstraintTest.kt b/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/ConstraintTest.kt new file mode 100644 index 0000000..d2f927f --- /dev/null +++ b/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/ConstraintTest.kt @@ -0,0 +1,123 @@ +package io.github.nomisrev.openapi + +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +class ConstraintTest { + @Test + fun intRange() { + assertEquals( + Model.Primitive.int( + constraint = NumberConstraint( + minimum = 1.0, + maximum = 10.0, + exclusiveMinimum = false, + exclusiveMaximum = false, + multipleOf = null + ) + ), + Schema( + type = Schema.Type.Basic.Integer, + minimum = 1.0, + maximum = 10.0, + exclusiveMinimum = false, + exclusiveMaximum = false + ).toModel("IntRange") + ) + } + + @Test + fun doubleRange() { + assertEquals( + Model.Primitive.double( + constraint = NumberConstraint( + minimum = 1.0, + maximum = 10.0, + exclusiveMinimum = false, + exclusiveMaximum = false, + multipleOf = null + ) + ), + Schema( + type = Schema.Type.Basic.Number, + minimum = 1.0, + maximum = 10.0, + exclusiveMinimum = false, + exclusiveMaximum = false + ).toModel("IntRange") + ) + } + + @Test + fun text() { + assertEquals( + Model.Primitive.string( + constraint = TextConstraint( + maxLength = 10, + minLength = 1, + pattern = null + ) + ), + Schema( + type = Schema.Type.Basic.String, + maxLength = 10, + minLength = 1, + pattern = null + ).toModel("Text") + ) + } + + @Test + fun list() { + assertEquals( + Model.Collection.list( + inner = Model.Primitive.string(), + constraint = CollectionConstraint( + minItems = 1, + maxItems = 10 + ) + ), + Schema( + type = Schema.Type.Basic.Array, + items = ReferenceOr.value(Schema(type = Schema.Type.Basic.String)), + maxItems = 10, + minItems = 1 + ).toModel("List") + ) + } + + @Test + fun set() { + assertEquals( + Model.Collection.set( + inner = Model.Primitive.string(), + constraint = CollectionConstraint( + minItems = 1, + maxItems = 10 + ) + ), + Schema( + type = Schema.Type.Basic.Array, + items = ReferenceOr.value(Schema(type = Schema.Type.Basic.String)), + maxItems = 10, + minItems = 1, + uniqueItems = true + ).toModel("List") + ) + } + + @Test + fun obj() { + assertEquals( + Model.obj( + context = NamingContext.Named("Obj"), + properties = listOf(Model.Object.property("name", Model.Primitive.string())), + inline = listOf(Model.Primitive.string()) + ), + Schema( + type = Schema.Type.Basic.Object, + properties = mapOf("name" to ReferenceOr.value(Schema(type = Schema.Type.Basic.String))) + ).toModel("Obj") + ) + } +} \ No newline at end of file diff --git a/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/DefaultArguments.kt b/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/DefaultArguments.kt index ebda238..ffccb72 100644 --- a/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/DefaultArguments.kt +++ b/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/DefaultArguments.kt @@ -8,109 +8,71 @@ import org.junit.jupiter.api.Test class DefaultArguments { @Test fun nullArgumentForNonNullList() { - val actual = - testAPI - .copy( - components = - Components( - schemas = - mapOf( - "Strings" to - value( - Schema( - type = Type.Basic.Object, - properties = - mapOf( - "value" to - value( - Schema( - type = Type.Basic.Array, - items = value(Schema(type = Type.Basic.String)), - default = ExampleValue("null"), - nullable = false - ) - ) - ) - ) - ) - ) + val actual = Schema( + type = Type.Basic.Object, + properties = + mapOf( + "value" to + value( + Schema( + type = Type.Basic.Array, + items = value(Schema(type = Type.Basic.String)), + default = ExampleValue("null"), + nullable = false ) - ) - .models() + ) + ) + ).toModel("Strings") val expected = - Model.Object( + Model.obj( context = NamingContext.Named("Strings"), properties = listOf( - Model.Object.Property( + Model.Object.property( "value", - Model.Collection.List( - inner = Model.Primitive.String(default = null, description = null), - default = emptyList(), - description = null - ), - isRequired = false, + Model.Collection.list(inner = Model.Primitive.string(), default = emptyList()), isNullable = false, - description = null ) ), - description = null, - inline = listOf(Model.Primitive.String(default = null, description = null)) + inline = listOf(Model.Primitive.string()), ) - assertEquals(setOf(expected), actual) + assertEquals(expected, actual) } @Test fun jsEmptyArrayArgumentForList() { - val actual = - testAPI - .copy( - components = - Components( - schemas = - mapOf( - "Strings" to - value( - Schema( - type = Type.Basic.Object, - properties = - mapOf( - "value" to - value( - Schema( - type = Type.Basic.Array, - items = value(Schema(type = Type.Basic.String)), - default = ExampleValue("[]"), - nullable = false - ) - ) - ) - ) - ) - ) + val actual = Schema( + type = Type.Basic.Object, + properties = + mapOf( + "value" to + value( + Schema( + type = Type.Basic.Array, + items = value(Schema(type = Type.Basic.String)), + default = ExampleValue("[]"), + nullable = false ) - ) - .models() + ) + ) + ).toModel("Strings") val expected = - Model.Object( + Model.obj( context = NamingContext.Named("Strings"), properties = listOf( - Model.Object.Property( + Model.Object.property( "value", - Model.Collection.List( - inner = Model.Primitive.String(default = null, description = null), + Model.Collection.list( + inner = Model.Primitive.string(), default = emptyList(), description = null ), - isRequired = false, isNullable = false, - description = null ) ), - description = null, - inline = listOf(Model.Primitive.String(default = null, description = null)) + inline = listOf(Model.Primitive.string()) ) - assertEquals(setOf(expected), actual) + assertEquals(expected, actual) } } diff --git a/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/ModelTest.kt b/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/ModelTest.kt index 6ac0302..5830536 100644 --- a/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/ModelTest.kt +++ b/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/ModelTest.kt @@ -1,7 +1,6 @@ package io.github.nomisrev.openapi import io.github.nomisrev.openapi.Model.Object -import io.github.nomisrev.openapi.Model.Object.Property import io.github.nomisrev.openapi.Model.Primitive import io.github.nomisrev.openapi.ReferenceOr.Companion.value import io.github.nomisrev.openapi.Schema.Type @@ -19,7 +18,7 @@ class ModelTest { @Test fun emptySchema() { assertThrows { - testAPI.copy(components = Components(schemas = mapOf("Empty" to value(Schema())))).models() + Schema().toModel("Empty") } } @@ -29,46 +28,44 @@ class ModelTest { testAPI .copy( components = - Components(schemas = mapOf("Person" to value(personSchema), "Id" to value(idSchema))) + Components(schemas = mapOf("Person" to value(personSchema), "Id" to value(idSchema))) ) .models() val expected = setOf( - Object( + Model.obj( context = NamingContext.Named("Person"), properties = - listOf( - Property( - "id", - id, - isRequired = true, - isNullable = false, - description = "An explicit ID type" - ), - Property( - "name", - // TODO default, and description doesn't belong to `Model`, - // but to Route.Param, or Property. - // Same probably applies to validation, refactor then. - Primitive.String(default = "John Doe", description = "The name of the person"), - isRequired = true, - isNullable = false, - description = "The name of the person" - ), - Property( - "age", - Primitive.Int(default = null, description = null), - isRequired = false, - isNullable = true, - description = null - ) + listOf( + Object.property( + "id", + id, + isRequired = true, + isNullable = false, + description = "An explicit ID type" + ), + Object.property( + "name", + // TODO default, and description doesn't belong to `Model`, + // but to Route.Param, or Property. + // Same probably applies to validation, refactor then. + Primitive.string(default = "John Doe", description = "The name of the person"), + isRequired = true, + isNullable = false, + description = "The name of the person" ), + Object.property( + baseName = "age", + model = Primitive.int(), + isNullable = true, + ) + ), description = "A person", inline = - listOf( - Primitive.String(default = "John Doe", description = "The name of the person"), - Primitive.Int(default = null, description = null) - ) + listOf( + Primitive.string(default = "John Doe", description = "The name of the person"), + Primitive.int() + ) ), id ) @@ -77,130 +74,64 @@ class ModelTest { @Test fun freeForm() { - val actual = - testAPI - .copy( - components = - Components( - schemas = - mapOf( - "FreeForm" to - value( - Schema( - type = Type.Basic.Object, - additionalProperties = AdditionalProperties.Allowed(true) - ) - ) - ) - ) - ) - .models() - val expected = Model.FreeFormJson(description = null) - assertEquals(setOf(expected), actual) + assertEquals( + Model.FreeFormJson(description = null), + Schema( + type = Type.Basic.Object, + additionalProperties = AdditionalProperties.Allowed(true) + ).toModel("FreeForm") + ) } @Test fun freeFormNotAllowedIsIllegal() { assertThrows { - testAPI - .copy( - components = - Components( - schemas = - mapOf( - "FreeForm" to - value( - Schema( - type = Type.Basic.Object, - additionalProperties = AdditionalProperties.Allowed(false) - ) - ) - ) - ) - ) - .models() - .let(::println) + Schema( + type = Type.Basic.Object, + additionalProperties = AdditionalProperties.Allowed(false) + ).toModel("FreeForm") } } @Test fun topLevelOneOfWithInlinePrimitives() { - val actual = - testAPI - .copy( - components = - Components( - schemas = - mapOf( - "OneOf" to - value( - Schema( - oneOf = - listOf( - value(Schema(type = Type.Basic.String)), - value(Schema(type = Type.Basic.Integer)) - ), - default = ExampleValue("example") - ) - ) - ) - ) - ) - .models() + val actual = Schema( + oneOf = listOf( + value(Schema(type = Type.Basic.String)), + value(Schema(type = Type.Basic.Integer)) + ), + default = ExampleValue("example") + ).toModel("OneOf") val expected = Model.Union( context = NamingContext.Named("OneOf"), description = null, default = "example", cases = - listOf( - // Order is important for deserialization, - // order swapped compared to originally - Model.Union.Case( - context = NamingContext.Named("OneOf"), - model = Primitive.Int(default = null, description = null) - ), - Model.Union.Case( - context = NamingContext.Named("OneOf"), - model = Primitive.String(default = null, description = null) - ) - ), - inline = - listOf( - Primitive.String(default = null, description = null), - Primitive.Int(default = null, description = null) - ) + listOf( + // Order is important for deserialization, + // order swapped compared to originally + Model.Union.Case(context = NamingContext.Named("OneOf"), model = Primitive.int()), + Model.Union.Case(context = NamingContext.Named("OneOf"), model = Primitive.string()) + ), + inline = listOf(Primitive.string(), Primitive.int()) ) - assertEquals(setOf(expected), actual) + assertEquals(expected, actual) } @Test fun openEnum() { - val actual = - testAPI - .copy( - components = - Components( - schemas = - mapOf( - "OpenEnum" to - value( - Schema( - description = "OpenEnum Desc", - anyOf = - listOf( - value(enumSchema.copy(description = "Inner Enum Desc")), - ReferenceOr.Value( - Schema(type = Type.Basic.String, description = "OpenCase Desc") - ) - ), - default = ExampleValue("Custom-open-enum-value") - ) - ) - ) - ) + val actual = Schema( + description = "OpenEnum Desc", + anyOf = + listOf( + value(enumSchema.copy(description = "Inner Enum Desc")), + ReferenceOr.Value( + Schema(type = Type.Basic.String, description = "OpenCase Desc") ) - .models() + ), + default = ExampleValue("Custom-open-enum-value") + ).toModel("OpenEnum") val expected = Model.Enum.Open( context = NamingContext.Named("OpenEnum"), @@ -208,35 +139,20 @@ class ModelTest { default = "Custom-open-enum-value", description = "OpenEnum Desc" ) - assertEquals(setOf(expected), actual) + assertEquals(expected, actual) } @Test fun anyOf() { - val actual = - testAPI - .copy( - components = - Components( - schemas = - mapOf( - "AnyOf" to - value( - Schema( - description = "AnyOf Desc", - anyOf = - listOf( - value(enumSchema), - ReferenceOr.Value( - Schema(type = Type.Basic.Integer, description = "Int Case Desc") - ) - ) - ) - ) - ) - ) + val actual = Schema( + description = "AnyOf Desc", + anyOf = listOf( + value(enumSchema), + ReferenceOr.Value( + Schema(type = Type.Basic.Integer, description = "Int Case Desc") ) - .models() + ) + ).toModel("AnyOf") val context = NamingContext.Nested( NamingContext.Named("AutoOrManual"), @@ -247,190 +163,86 @@ class ModelTest { context = NamingContext.Named("AnyOf"), description = "AnyOf Desc", cases = - listOf( - Model.Union.Case(context = context, model = enum.copy(context = context)), - Model.Union.Case( - context = NamingContext.Named("AnyOf"), - model = Primitive.Int(default = null, description = "Int Case Desc") - ) - ), - default = "Auto", - inline = - listOf( - enum.copy(context = context), - Primitive.Int(default = null, description = "Int Case Desc") + listOf( + Model.Union.Case(context = context, model = enum.copy(context = context)), + Model.Union.Case( + context = NamingContext.Named("AnyOf"), + model = Primitive.int(description = "Int Case Desc") ) + ), + default = "Auto", + inline = listOf(enum.copy(context = context), Primitive.int(description = "Int Case Desc")) ) - assertEquals(setOf(expected), actual) + assertEquals(expected, actual) } @Test fun typeArray() { - val actual = - testAPI - .copy( - components = - Components( - schemas = - mapOf( - "TypeArray" to - value( - Schema( - type = Type.Array(types = listOf(Type.Basic.Integer, Type.Basic.String)) - ) - ) - ) - ) - ) - .models() + val actual = Schema( + type = Type.Array(types = listOf(Type.Basic.Integer, Type.Basic.String)) + ).toModel("TypeArray") + val context = NamingContext.Named("TypeArray") val expected = - Model.Union( - context = NamingContext.Named("TypeArray"), - description = null, + Model.union( + context = context, cases = - listOf( - Model.Union.Case( - context = NamingContext.Named("TypeArray"), - model = Primitive.Int(default = null, description = null) - ), - Model.Union.Case( - context = NamingContext.Named("TypeArray"), - model = Primitive.String(default = null, description = null) - ) - ), - default = null, - inline = - listOf( - Primitive.Int(default = null, description = null), - Primitive.String(default = null, description = null) - ) + listOf( + Model.Union.Case(context = context, model = Primitive.int()), + Model.Union.Case(context = context, model = Primitive.string()) + ), + inline = listOf(Primitive.int(), Primitive.string()) ) - assertEquals(setOf(expected), actual) + assertEquals(expected, actual) } @Test - fun typeArraySingleton() { - val actual = - testAPI - .copy( - components = - Components( - schemas = - mapOf( - "TypeArray" to - value(Schema(type = Type.Array(types = listOf(Type.Basic.Integer)))) - ) - ) - ) - .models() - val expected = Primitive.Int(default = null, description = null) - assertEquals(setOf(expected), actual) + fun typeArraySingletonIsFlattened() { + assertEquals( + Primitive.int(), + Schema(type = Type.Array(types = listOf(Type.Basic.Integer))).toModel("Int") + ) } @Test - fun binary() { - val actual = - testAPI - .copy( - components = - Components( - schemas = - mapOf( - "Primitive.Binary" to value(Schema(type = Type.Basic.String, format = "binary")) - ) - ) - ) - .models() - val expected = Model.OctetStream(description = null) - assertEquals(setOf(expected), actual) + fun stringBinaryFormat() { + assertEquals( + Model.OctetStream(description = null), + Schema(type = Type.Basic.String, format = "binary").toModel("Binary") + ) } @Test fun defaultArrayIsList() { - val actual = - testAPI - .copy( - components = - Components( - schemas = - mapOf( - "Primitive.Array" to - value( - Schema( - type = Type.Basic.Array, - items = value(Schema(type = Type.Basic.Integer)) - ) - ) - ) - ) - ) - .models() - val expected = - Model.Collection.List( - inner = Primitive.Int(default = null, description = null), - description = null, - default = null - ) - assertEquals(setOf(expected), actual) + assertEquals( + Model.Collection.list(Primitive.int()), + Schema( + type = Type.Basic.Array, + items = value(Schema(type = Type.Basic.Integer)) + ).toModel("List") + ) } @Test fun noUniqueItemsIsList() { - val actual = - testAPI - .copy( - components = - Components( - schemas = - mapOf( - "Primitive.Array" to - value( - Schema( - type = Type.Basic.Array, - items = value(Schema(type = Type.Basic.Integer)), - uniqueItems = false - ) - ) - ) - ) - ) - .models() - val expected = - Model.Collection.List( - inner = Primitive.Int(default = null, description = null), - description = null, - default = null - ) - assertEquals(setOf(expected), actual) + assertEquals( + Model.Collection.list(inner = Primitive.int()), + Schema( + type = Type.Basic.Array, + items = value(Schema(type = Type.Basic.Integer)), + uniqueItems = false + ).toModel("List") + ) } @Test fun uniqueItemsIsSet() { - val actual = - testAPI - .copy( - components = - Components( - schemas = - mapOf( - "Primitive.Set" to - value( - Schema( - type = Type.Basic.Array, - items = value(Schema(type = Type.Basic.Integer)), - uniqueItems = true - ) - ) - ) - ) - ) - .models() - val expected = - Model.Collection.Set( - inner = Primitive.Int(default = null, description = null), - description = null, - default = null - ) - assertEquals(setOf(expected), actual) + assertEquals( + Model.Collection.set(inner = Primitive.int()), + Schema( + type = Type.Basic.Array, + items = value(Schema(type = Type.Basic.Integer)), + uniqueItems = true + ).toModel("Set") + ) } } diff --git a/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/NameGenerationSpec.kt b/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/NameGenerationSpec.kt deleted file mode 100644 index 869f8c6..0000000 --- a/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/NameGenerationSpec.kt +++ /dev/null @@ -1,402 +0,0 @@ -package io.github.nomisrev.openapi - -import io.github.nomisrev.openapi.Model.Object -import io.github.nomisrev.openapi.Model.Object.Property -import io.github.nomisrev.openapi.Model.Primitive -import io.github.nomisrev.openapi.ReferenceOr.Companion.value -import io.github.nomisrev.openapi.Schema.Type -import kotlin.test.Test -import kotlin.test.assertEquals -import org.junit.jupiter.api.assertThrows - -/** - * With any kind of disjunction, we need to generate names for the cases since we need to convert to - * tagged unions. - * - * The tagged union name uses any naming information we have, so any nested cases needs to generate - * a name explicitly. - * - * This concern is currently a bit clumsily defined in both the OpenAPITransformer, and the Model - * generation. - * - * We currently have a couple special cases: - * - Primitives result in `CaseInt`, `CaseString`, etc. - * - With collections it becomes `CaseInts`, `CaseStrings`, etc. - * - With complex types we use `ReferenceOr.Ref`, to avoid generating a name. - * - For enums we concatenate the enum values, and capitalize the first letter. => AutoOrManual, - * AllOrNone, but this could result in very long names... - - */ -class UnionNameGenerationSpec { - @Test - fun nestedInlineObjInUnionGeneratesNameOnTypeProperty() { - val actual = - testAPI - .copy( - components = - Components( - schemas = - mapOf( - "TypeOrEvent" to - value( - Schema( - oneOf = - listOf( - value( - Schema( - type = Type.Basic.Object, - properties = - mapOf( - "type" to - value( - Schema(type = Type.Basic.String, enum = listOf("Function")) - ), - "value" to value(Schema(type = Type.Basic.String)) - ) - ) - ), - value(Schema(type = Type.Basic.Integer)) - ) - ) - ) - ) - ) - ) - .models() - val obj = - Object( - context = - NamingContext.Nested( - NamingContext.Named("Function"), - NamingContext.Named("TypeOrEvent"), - ), - description = null, - properties = - listOf( - Property( - "type", - Model.Enum.Closed( - context = - NamingContext.Nested( - NamingContext.Named("type"), - NamingContext.Nested( - NamingContext.Named("Function"), - NamingContext.Named("TypeOrEvent"), - ) - ), - inner = Primitive.String(default = null, description = null), - values = listOf("Function"), - default = null, - description = null - ), - isRequired = false, - isNullable = true, - description = null - ), - Property( - "value", - Primitive.String(default = null, description = null), - isRequired = false, - isNullable = true, - description = null - ) - ), - inline = - listOf( - Model.Enum.Closed( - context = - NamingContext.Nested( - NamingContext.Named("type"), - NamingContext.Nested( - NamingContext.Named("Function"), - NamingContext.Named("TypeOrEvent"), - ) - ), - inner = Primitive.String(default = null, description = null), - values = listOf("Function"), - default = null, - description = null - ), - Primitive.String(default = null, description = null) - ) - ) - val expected = - Model.Union( - context = NamingContext.Named("TypeOrEvent"), - description = null, - default = null, - cases = - listOf( - Model.Union.Case( - context = - NamingContext.Nested( - NamingContext.Named("Function"), - NamingContext.Named("TypeOrEvent"), - ), - model = obj - ), - Model.Union.Case( - context = NamingContext.Named("TypeOrEvent"), - model = Primitive.Int(default = null, description = null) - ) - ), - inline = listOf(obj, Primitive.Int(default = null, description = null)) - ) - - assertEquals(setOf(expected), actual) - } - - @Test - fun nestedInlineObjInUnionGeneratesNameOnEventProperty() { - val actual = - testAPI - .copy( - components = - Components( - schemas = - mapOf( - "TypeOrEvent" to - value( - Schema( - oneOf = - listOf( - value( - Schema( - type = Type.Basic.Object, - properties = - mapOf( - "event" to - value( - Schema(type = Type.Basic.String, enum = listOf("RunThread")) - ), - "value" to value(Schema(type = Type.Basic.String)) - ) - ) - ), - value(Schema(type = Type.Basic.Integer)) - ) - ) - ) - ) - ) - ) - .models() - val obj = - Object( - context = - NamingContext.Nested( - NamingContext.Named("RunThread"), - NamingContext.Named("TypeOrEvent"), - ), - description = null, - properties = - listOf( - Property( - "event", - Model.Enum.Closed( - context = - NamingContext.Nested( - NamingContext.Named("event"), - NamingContext.Nested( - NamingContext.Named("RunThread"), - NamingContext.Named("TypeOrEvent"), - ) - ), - inner = Primitive.String(default = null, description = null), - values = listOf("RunThread"), - default = null, - description = null - ), - isRequired = false, - isNullable = true, - description = null - ), - Property( - "value", - Primitive.String(default = null, description = null), - isRequired = false, - isNullable = true, - description = null - ) - ), - inline = - listOf( - Model.Enum.Closed( - context = - NamingContext.Nested( - NamingContext.Named("event"), - NamingContext.Nested( - NamingContext.Named("RunThread"), - NamingContext.Named("TypeOrEvent"), - ) - ), - inner = Primitive.String(default = null, description = null), - values = listOf("RunThread"), - default = null, - description = null - ), - Primitive.String(default = null, description = null) - ) - ) - val expected = - Model.Union( - context = NamingContext.Named("TypeOrEvent"), - description = null, - default = null, - cases = - listOf( - Model.Union.Case( - context = - NamingContext.Nested( - NamingContext.Named("RunThread"), - NamingContext.Named("TypeOrEvent"), - ), - model = obj - ), - Model.Union.Case( - context = NamingContext.Named("TypeOrEvent"), - model = Primitive.Int(default = null, description = null) - ) - ), - inline = listOf(obj, Primitive.Int(default = null, description = null)) - ) - - assertEquals(setOf(expected), actual) - } - - @Test - fun topLevelOneOfWithInlineEnumAndDefault() { - val actual = - testAPI - .copy( - components = - Components( - schemas = - mapOf( - "OneOf" to - value( - Schema( - oneOf = listOf(value(Schema(type = Type.Basic.String)), value(enumSchema)) - ) - ) - ) - ) - ) - .models() - val expected = - Model.Union( - context = NamingContext.Named("OneOf"), - description = null, - default = "Auto", - cases = - listOf( - Model.Union.Case( - context = - NamingContext.Nested( - NamingContext.Named("AutoOrManual"), - NamingContext.Named("OneOf") - ), - model = enum - ), - // Order is important for deserialization, - // order swapped compared to originally - Model.Union.Case( - context = NamingContext.Named("OneOf"), - model = Primitive.String(default = null, description = null) - ) - ), - inline = listOf(Primitive.String(default = null, description = null), enum) - ) - assertEquals(setOf(expected), actual) - } - - @Test fun topLevelOneOfWithListAndInlineInner() = topLevelOneOfWithCollectionAndInlineInner(false) - - @Test fun topLevelOneOfWithSetAndInlineInner() = topLevelOneOfWithCollectionAndInlineInner(true) - - @Test - fun topLevelOneOfWithCollectionAndInlineInner() = topLevelOneOfWithCollectionAndInlineInner(null) - - private fun topLevelOneOfWithCollectionAndInlineInner(uniqueItems: Boolean?) { - val actual = - testAPI - .copy( - components = - Components( - schemas = - mapOf( - "OneOf" to - value( - Schema( - oneOf = - listOf( - value( - Schema( - type = Type.Basic.Array, - items = value(idSchema), - uniqueItems = uniqueItems - ) - ), - value(Schema(type = Type.Basic.String)), - ) - ) - ) - ) - ) - ) - .models() - val name = if (uniqueItems == true) "Set" else "List" - val context = NamingContext.Nested(NamingContext.Named(name), NamingContext.Named("OneOf")) - val id = id.copy(context = context) - val model = - if (uniqueItems == true) Model.Collection.Set(inner = id, default = null, description = null) - else Model.Collection.List(inner = id, default = null, description = null) - val expected = - Model.Union( - context = NamingContext.Named("OneOf"), - description = null, - default = null, - cases = - listOf( - Model.Union.Case(context = context, model = model), - Model.Union.Case( - context = NamingContext.Named("OneOf"), - model = Primitive.String(default = null, description = null) - ) - ), - inline = listOf(id, Primitive.String(default = null, description = null)) - ) - assertEquals(setOf(expected), actual) - } - - @Test - fun nonSupportedCases() { - assertThrows { - testAPI - .copy( - components = - Components( - schemas = - mapOf( - "TypeOrEvent" to - value( - Schema( - oneOf = - listOf( - value( - Schema( - type = Type.Basic.Object, - properties = - mapOf("value" to value(Schema(type = Type.Basic.String))) - ) - ), - value(Schema(type = Type.Basic.Integer)) - ) - ) - ) - ) - ) - ) - .models() - } - } -} diff --git a/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/PrimitiveTest.kt b/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/PrimitiveTest.kt index 797d656..48fb68a 100644 --- a/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/PrimitiveTest.kt +++ b/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/PrimitiveTest.kt @@ -1,7 +1,6 @@ package io.github.nomisrev.openapi import io.github.nomisrev.openapi.Model.Primitive -import io.github.nomisrev.openapi.ReferenceOr.Companion.value import io.github.nomisrev.openapi.Schema.Type import kotlin.test.Test import kotlin.test.assertEquals @@ -10,24 +9,21 @@ import org.junit.jupiter.api.assertThrows class PrimitiveTest { @Test fun double() { - primitive( - Schema(type = Type.Basic.Number, default = ExampleValue("1"), description = "My Desc"), - Primitive.Double(1.0, description = "My Desc") - ) + val actual = Schema(type = Type.Basic.Number, default = ExampleValue("1"), description = "My Desc") + .toModel("Double") + val expected = Primitive.double(default = 1.0, description = "My Desc") + assertEquals(expected, actual) } @Test fun doubleIncorrectDefault() { val e = assertThrows { - primitive( - Schema( - type = Type.Basic.Number, - default = ExampleValue("Nonsense Value"), - description = "My Desc" - ), - Primitive.Double(null, description = "My Desc") - ) + Schema( + type = Type.Basic.Number, + default = ExampleValue("Nonsense Value"), + description = "My Desc" + ).toModel("Primitive") } assertEquals("Default value Nonsense Value is not a Number.", e.message) } @@ -36,23 +32,22 @@ class PrimitiveTest { fun doubleIncorrectDefaultMultiple() { val e = assertThrows { - primitive( - Schema( - type = Type.Basic.Number, - default = ExampleValue.Multiple(listOf("Nonsense", "Value")), - description = "My Desc" - ), - Primitive.Double(null, description = "My Desc") - ) + Schema( + type = Type.Basic.Number, + default = ExampleValue.Multiple(listOf("Nonsense", "Value")), + description = "My Desc" + ).toModel("Primitive") } assertEquals("Multiple default values not supported for Number.", e.message) } @Test fun boolean() { - primitive( - Schema(type = Type.Basic.Boolean, default = ExampleValue("true"), description = "My Desc"), - Primitive.Boolean(true, description = "My Desc") + val actual = + Schema(type = Type.Basic.Boolean, default = ExampleValue("true"), description = "My Desc").toModel("Primitive") + assertEquals( + Primitive.Boolean(true, description = "My Desc"), + actual ) } @@ -60,14 +55,11 @@ class PrimitiveTest { fun booleanIncorrectDefault() { val e = assertThrows { - primitive( - Schema( - type = Type.Basic.Boolean, - default = ExampleValue("Nonsense Value"), - description = "My Desc" - ), - Primitive.Boolean(null, description = "My Desc") - ) + Schema( + type = Type.Basic.Boolean, + default = ExampleValue("Nonsense Value"), + description = "My Desc" + ).toModel("Primitive") } assertEquals("Default value Nonsense Value is not a Boolean.", e.message) } @@ -76,23 +68,22 @@ class PrimitiveTest { fun booleanIncorrectDefaultMultiple() { val e = assertThrows { - primitive( - Schema( - type = Type.Basic.Boolean, - default = ExampleValue.Multiple(listOf("Nonsense", "Value")), - description = "My Desc" - ), - Primitive.Boolean(null, description = "My Desc") - ) + Schema( + type = Type.Basic.Boolean, + default = ExampleValue.Multiple(listOf("Nonsense", "Value")), + description = "My Desc" + ).toModel("Primitive") } assertEquals("Multiple default values not supported for Boolean.", e.message) } @Test fun integer() { - primitive( - Schema(type = Type.Basic.Integer, default = ExampleValue("2"), description = "My Desc"), - Primitive.Int(2, description = "My Desc") + val actual = + Schema(type = Type.Basic.Integer, default = ExampleValue("2"), description = "My Desc").toModel("Primitive") + assertEquals( + Primitive.int(default = 2, description = "My Desc"), + actual ) } @@ -100,14 +91,11 @@ class PrimitiveTest { fun integerIncorrectDefault() { val e = assertThrows { - primitive( - Schema( - type = Type.Basic.Integer, - default = ExampleValue("Nonsense Value"), - description = "My Desc" - ), - Primitive.Int(null, description = "My Desc") - ) + Schema( + type = Type.Basic.Integer, + default = ExampleValue("Nonsense Value"), + description = "My Desc" + ).toModel("Primitive") } assertEquals("Default value Nonsense Value is not a Integer.", e.message) } @@ -116,67 +104,59 @@ class PrimitiveTest { fun integerIncorrectDefaultMultiple() { val e = assertThrows { - primitive( - Schema( - type = Type.Basic.Integer, - default = ExampleValue.Multiple(listOf("Nonsense", "Value")), - description = "My Desc" - ), - Primitive.Int(null, description = "My Desc") - ) + Schema( + type = Type.Basic.Integer, + default = ExampleValue.Multiple(listOf("Nonsense", "Value")), + description = "My Desc" + ).toModel("Primitive") } assertEquals("Multiple default values not supported for Integer.", e.message) } @Test fun string() { - primitive( - Schema( - type = Type.Basic.String, - default = ExampleValue("Some Text"), - description = "My Desc" - ), - Primitive.String("Some Text", description = "My Desc") + val actual = Schema( + type = Type.Basic.String, + default = ExampleValue("Some Text"), + description = "My Desc" + ).toModel("Primitive") + assertEquals( + Primitive.string(default = "Some Text", description = "My Desc"), + actual ) } @Test fun stringIntegerDefaultRemainsString() { - primitive( - Schema(type = Type.Basic.String, default = ExampleValue("999"), description = "My Desc"), - Primitive.String("999", description = "My Desc") + val actual = + Schema(type = Type.Basic.String, default = ExampleValue("999"), description = "My Desc").toModel("Primitive") + assertEquals( + Primitive.string(default = "999", description = "My Desc"), + actual ) } @Test fun stringIncorrectDefaultMultiple() { - primitive( - Schema( - type = Type.Basic.String, - default = ExampleValue.Multiple(listOf("Nonsense", "Value")), - description = "My Desc" - ), - Primitive.String("Nonsense, Value", description = "My Desc") + val actual = Schema( + type = Type.Basic.String, + default = ExampleValue.Multiple(listOf("Nonsense", "Value")), + description = "My Desc" + ).toModel("Primitive") + assertEquals( + Primitive.string(default = "Nonsense, Value", description = "My Desc"), + actual ) } @Test fun nullNotSupported() { assertThrows { - primitive( - Schema( - type = Type.Basic.Null, - default = ExampleValue.Multiple(listOf("Nonsense", "Value")), - description = "My Desc" - ), - Primitive.String("Nonsense, Value", description = "My Desc") - ) + Schema( + type = Type.Basic.Null, + default = ExampleValue.Multiple(listOf("Nonsense", "Value")), + description = "My Desc" + ).toModel("Primitive") } } - - private fun primitive(schema: Schema, model: Model) { - val actual = - testAPI.copy(components = Components(schemas = mapOf("Primitive" to value(schema)))).models() - assertEquals(setOf(model), actual) - } } diff --git a/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/RootTest.kt b/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/RootTest.kt index ebcc513..60d7983 100644 --- a/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/RootTest.kt +++ b/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/RootTest.kt @@ -78,10 +78,10 @@ class RootTest { returnType = Route.Returns( HttpStatusCode.OK to - Route.ReturnType(Model.Primitive.String(null, null), emptyMap()) + Route.ReturnType(Model.Primitive.string(null, null), emptyMap()) ), extensions = emptyMap(), - nested = listOf(Model.Primitive.String(null, null)) + nested = listOf(Model.Primitive.string(null, null)) ) ), nested = emptyList() diff --git a/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/testmodels.kt b/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/TestModels.kt similarity index 86% rename from typed/src/commonTest/kotlin/io/github/nomisrev/openapi/testmodels.kt rename to typed/src/commonTest/kotlin/io/github/nomisrev/openapi/TestModels.kt index 2d44333..342b493 100644 --- a/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/testmodels.kt +++ b/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/TestModels.kt @@ -1,6 +1,5 @@ package io.github.nomisrev.openapi -import io.github.nomisrev.openapi.Model.Object import io.github.nomisrev.openapi.Model.Object.Property import io.github.nomisrev.openapi.Model.Primitive import io.github.nomisrev.openapi.ReferenceOr.Companion.value @@ -35,20 +34,20 @@ val personSchema = ) val id = - Object( + Model.obj( context = NamingContext.Named("Id"), description = "An explicit ID type", properties = listOf( Property( "value", - Primitive.String(default = null, description = null), + Primitive.string(), isRequired = false, isNullable = true, description = null ) ), - inline = listOf(Primitive.String(default = null, description = null)) + inline = listOf(Primitive.string()) ) val testAPI = OpenAPI(info = Info("Test API", version = "1.0.0")) @@ -60,7 +59,7 @@ val enum = Model.Enum.Closed( context = NamingContext.Nested(NamingContext.Named("AutoOrManual"), NamingContext.Named("OneOf")), - inner = Primitive.String(default = "Auto", description = null), + inner = Primitive.string(default = "Auto", description = null), values = listOf("Auto", "Manual"), default = "Auto", description = null diff --git a/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/TestSyntax.kt b/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/TestSyntax.kt new file mode 100644 index 0000000..4b6b6cb --- /dev/null +++ b/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/TestSyntax.kt @@ -0,0 +1,90 @@ +package io.github.nomisrev.openapi + +public fun Model.Primitive.Companion.string( + default: String? = null, + description: String? = null, + constraint: TextConstraint = TextConstraint(Int.MAX_VALUE, 0, null) +): Model.Primitive.String = + Model.Primitive.String(default = default, description = description, bounds = constraint) + +public fun Model.Primitive.Companion.int( + default: Int? = null, + description: String? = null, + constraint: NumberConstraint = + NumberConstraint(false, Double.NEGATIVE_INFINITY, false, Double.POSITIVE_INFINITY, null) +): Model.Primitive.Int = + Model.Primitive.Int(default = default, description = description, bounds = constraint) + +public fun Model.Primitive.Companion.double( + default: Double? = null, + description: String? = null, + constraint: NumberConstraint = + NumberConstraint(false, Double.NEGATIVE_INFINITY, false, Double.POSITIVE_INFINITY, null) +): Model.Primitive.Double = + Model.Primitive.Double(default = default, description = description, bounds = constraint) + +public fun Model.Collection.Companion.list( + inner: Model, + default: List? = null, + description: String? = null, + constraint: CollectionConstraint = CollectionConstraint(0, Int.MAX_VALUE) +): Model.Collection.List = + Model.Collection.List( + inner = inner, + default = default, + description = description, + bounds = constraint + ) + +public fun Model.Collection.Companion.set( + inner: Model, + default: List? = null, + description: String? = null, + constraint: CollectionConstraint = CollectionConstraint(0, Int.MAX_VALUE) +): Model.Collection.Set = + Model.Collection.Set(inner = inner, default = default, description = description, bounds = constraint) + +public fun Model.Object.Companion.property( + baseName: String, + model: Model, + isRequired: Boolean = false, + isNullable: Boolean = true, + description: String? = null +): Model.Object.Property = + Model.Object.Property( + baseName = baseName, + model = model, + isRequired = isRequired, + isNullable = isNullable, + description = description + ) + + +public fun Model.Companion.obj( + context: NamingContext, + properties: List, + inline: List, + constraint: ObjectConstraint = ObjectConstraint(0, Int.MAX_VALUE), + description: String? = null +): Model.Object = + Model.Object( + context = context, + description = description, + properties = properties, + inline = inline, + constraint = constraint, + ) + +public fun Model.Companion.union( + context: NamingContext, + cases: List, + inline: List, + default: String? = null, + description: String? = null +): Model.Union = + Model.Union(context = context, cases = cases, default = default, inline = inline, description = description) + +fun Schema.toModel(name: String): Model = + testAPI.copy( + components = Components(schemas = mapOf(name to ReferenceOr.value(this))) + ).models().first() \ No newline at end of file diff --git a/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/UnionNameGenerationSpec.kt b/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/UnionNameGenerationSpec.kt new file mode 100644 index 0000000..3d3103c --- /dev/null +++ b/typed/src/commonTest/kotlin/io/github/nomisrev/openapi/UnionNameGenerationSpec.kt @@ -0,0 +1,287 @@ +package io.github.nomisrev.openapi + +import io.github.nomisrev.openapi.Model.Object +import io.github.nomisrev.openapi.Model.Object.Property +import io.github.nomisrev.openapi.Model.Primitive +import io.github.nomisrev.openapi.NamingContext.Named +import io.github.nomisrev.openapi.NamingContext.Nested +import io.github.nomisrev.openapi.ReferenceOr.Companion.value +import io.github.nomisrev.openapi.Schema.Type +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.jupiter.api.assertThrows + +/** + * With any kind of disjunction, we need to generate names for the cases since we need to convert to + * tagged unions. + * + * The tagged union name uses any naming information we have, so any nested cases needs to generate + * a name explicitly. + * + * This concern is currently a bit clumsily defined in both the OpenAPITransformer, and the Model + * generation. + * + * We currently have a couple special cases: + * - Primitives result in `CaseInt`, `CaseString`, etc. + * - With collections it becomes `CaseInts`, `CaseStrings`, etc. + * - With complex types we use `ReferenceOr.Ref`, to avoid generating a name. + * - For enums we concatenate the enum values, and capitalize the first letter. => AutoOrManual, + * AllOrNone, but this could result in very long names... - + */ +class UnionNameGenerationSpec { + @Test + fun nestedInlineObjInUnionGeneratesNameOnTypeProperty() { + val actual = + Schema( + oneOf = + listOf( + value( + Schema( + type = Type.Basic.Object, + properties = + mapOf( + "type" to + value( + Schema(type = Type.Basic.String, enum = listOf("Function")) + ), + "value" to value(Schema(type = Type.Basic.String)) + ) + ) + ), + value(Schema(type = Type.Basic.Integer)) + ) + ).toModel("TypeOrEvent") + val context = Nested(Named("Function"), Named("TypeOrEvent")) + val propContext = Nested(Named("type"), context) + val obj = + Model.obj( + context = context, + properties = + listOf( + Object.property( + "type", + Model.Enum.Closed( + context = propContext, + inner = Primitive.string(), + values = listOf("Function"), + default = null, + description = null + ), + ), + Object.property( + "value", + Primitive.string(), + ) + ), + inline = + listOf( + Model.Enum.Closed( + context = propContext, + inner = Primitive.string(), + values = listOf("Function"), + default = null, + description = null + ), + Primitive.string() + ) + ) + val expected = + Model.Union( + context = Named("TypeOrEvent"), + description = null, + default = null, + cases = + listOf( + Model.Union.Case(context = context, model = obj), + Model.Union.Case(context = Named("TypeOrEvent"), model = Primitive.int()) + ), + inline = listOf(obj, Primitive.int()) + ) + + assertEquals(expected, actual) + } + + @Test + fun nestedInlineObjInUnionGeneratesNameOnEventProperty() { + val actual = + Schema( + oneOf = + listOf( + value( + Schema( + type = Type.Basic.Object, + properties = + mapOf( + "event" to + value( + Schema(type = Type.Basic.String, enum = listOf("RunThread")) + ), + "value" to value(Schema(type = Type.Basic.String)) + ) + ) + ), + value(Schema(type = Type.Basic.Integer)) + ) + ).toModel("TypeOrEvent") + val obj = + Model.obj( + context = + Nested( + Named("RunThread"), + Named("TypeOrEvent"), + ), + properties = + listOf( + Property( + "event", + Model.Enum.Closed( + context = + Nested( + Named("event"), + Nested( + Named("RunThread"), + Named("TypeOrEvent"), + ) + ), + inner = Primitive.string(), + values = listOf("RunThread"), + default = null, + description = null + ), + isRequired = false, + isNullable = true, + description = null + ), + Property( + "value", + Primitive.string(), + isRequired = false, + isNullable = true, + description = null + ) + ), + inline = + listOf( + Model.Enum.Closed( + context = + Nested( + Named("event"), + Nested( + Named("RunThread"), + Named("TypeOrEvent"), + ) + ), + inner = Primitive.string(), + values = listOf("RunThread"), + default = null, + description = null + ), + Primitive.string() + ) + ) + val expected = + Model.Union( + context = Named("TypeOrEvent"), + description = null, + default = null, + cases = + listOf( + Model.Union.Case( + context = + Nested( + Named("RunThread"), + Named("TypeOrEvent"), + ), + model = obj + ), + Model.Union.Case(context = Named("TypeOrEvent"), model = Primitive.int()) + ), + inline = listOf(obj, Primitive.int()) + ) + + assertEquals(expected, actual) + } + + @Test + fun topLevelOneOfWithInlineEnumAndDefault() { + val actual = Schema( + oneOf = listOf(value(Schema(type = Type.Basic.String)), value(enumSchema)) + ).toModel("OneOf") + val expected = + Model.Union( + context = Named("OneOf"), + description = null, + default = "Auto", + cases = + listOf( + Model.Union.Case(context = Nested(Named("AutoOrManual"), Named("OneOf")), model = enum), + // Order is important for deserialization, + // order swapped compared to originally + Model.Union.Case(context = Named("OneOf"), model = Primitive.string()) + ), + inline = listOf(Primitive.string(), enum) + ) + assertEquals(expected, actual) + } + + @Test + fun topLevelOneOfWithListAndInlineInner() = topLevelOneOfWithCollectionAndInlineInner(false) + + @Test + fun topLevelOneOfWithSetAndInlineInner() = topLevelOneOfWithCollectionAndInlineInner(true) + + @Test + fun topLevelOneOfWithCollectionAndInlineInner() = topLevelOneOfWithCollectionAndInlineInner(null) + + private fun topLevelOneOfWithCollectionAndInlineInner(uniqueItems: Boolean?) { + val actual = Schema( + oneOf = listOf( + value( + Schema( + type = Type.Basic.Array, + items = value(idSchema), + uniqueItems = uniqueItems + ) + ), + value(Schema(type = Type.Basic.String)), + ) + ).toModel("OneOf") + val name = if (uniqueItems == true) "Set" else "List" + val context = Nested(Named(name), Named("OneOf")) + val id = id.copy(context = context) + val model = + if (uniqueItems == true) Model.Collection.set(inner = id, default = null, description = null) + else Model.Collection.list(inner = id, default = null, description = null) + val expected = + Model.Union( + context = Named("OneOf"), + description = null, + default = null, + cases = + listOf( + Model.Union.Case(context = context, model = model), + Model.Union.Case(context = Named("OneOf"), model = Primitive.string()) + ), + inline = listOf(id, Primitive.string()) + ) + assertEquals(expected, actual) + } + + @Test + fun nonSupportedCases() { + assertThrows { + Schema( + oneOf = listOf( + value( + Schema( + type = Type.Basic.Object, + properties = + mapOf("value" to value(Schema(type = Type.Basic.String))) + ) + ), + value(Schema(type = Type.Basic.Integer)) + ) + ).toModel("TypeOrEvent") + } + } +}