Skip to content

Commit

Permalink
Add constraints
Browse files Browse the repository at this point in the history
  • Loading branch information
nomisRev committed Jul 10, 2024
1 parent fb554f5 commit 3742f09
Show file tree
Hide file tree
Showing 13 changed files with 842 additions and 917 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
)
}
50 changes: 37 additions & 13 deletions typed/src/commonMain/kotlin/io/github/nomisrev/openapi/Model.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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<String>?,
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<String>?,
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<Property>,
val inline: List<Model>
val inline: List<Model>,
val constraint: ObjectConstraint?
) : Model {
@Serializable
data class Property(
val baseName: String,
val model: Model,
Expand All @@ -195,6 +215,8 @@ sealed interface Model {
val isNullable: Boolean,
val description: String?
)

companion object
}

data class Union(
Expand Down Expand Up @@ -228,4 +250,6 @@ sealed interface Model {
override val description: String?
) : Enum
}

companion object
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -398,7 +404,8 @@ private class OpenAPITransformer(private val openAPI: OpenAPI) {
is Resolved.Value -> NamingContext.Nested(Named(name), context)
}
nestedModel(resolved, pContext)
}
},
ObjectConstraint(this)
)
}

Expand Down Expand Up @@ -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<String>): Enum.Closed {
Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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")
)
}
}
Loading

0 comments on commit 3742f09

Please sign in to comment.