Skip to content

Commit

Permalink
Generate docs
Browse files Browse the repository at this point in the history
  • Loading branch information
nomisRev committed Jun 11, 2024
1 parent 70d5de2 commit e25bac6
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,37 +34,51 @@ data class Route(
}

sealed interface Body {
val description: String?
val extensions: Map<String, JsonElement>

data class OctetStream(override val extensions: Map<String, JsonElement>) : Body
data class OctetStream(
override val description: String?,
override val extensions: Map<String, JsonElement>
) : Body

sealed interface Json : Body {
val type: Resolved<Model>

data class FreeForm(override val extensions: Map<String, JsonElement>) : Json {
override val type: Resolved<Model> = Resolved.Value(Model.FreeFormJson)
data class FreeForm(
override val description: String?,
override val extensions: Map<String, JsonElement>
) : Json {
override val type: Resolved<Model> = Resolved.Value(Model.FreeFormJson(description))
}

data class Defined(
override val type: Resolved<Model>,
override val description: String?,
override val extensions: Map<String, JsonElement>
) : Json
}

data class Xml(val type: Model, override val extensions: Map<String, JsonElement>) : Body
data class Xml(
val type: Model,
override val description: String?,
override val extensions: Map<String, JsonElement>
) : Body

sealed interface Multipart : Body {
val parameters: List<Multipart.FormData>
val parameters: List<FormData>

data class FormData(val name: String, val type: Resolved<Model>)

data class Value(
val parameters: List<FormData>,
override val description: String?,
override val extensions: Map<String, JsonElement>
) : Body, List<FormData> by parameters

data class Ref(
val value: Resolved.Ref<Model>,
override val description: String?,
override val extensions: Map<String, JsonElement>
) : Multipart {
override val parameters: List<FormData> = listOf(FormData(value.name, value))
Expand Down Expand Up @@ -124,17 +138,21 @@ sealed interface Resolved<A> {
* needs to generate a name has a [NamingContext], see [NamingContext] for more details.
*/
sealed interface Model {
val description: String?

sealed interface Primitive : Model {
data class Int(val default: kotlin.Int?) : Primitive
data class Int(val default: kotlin.Int?, override val description: kotlin.String?) : Primitive

data class Double(val default: kotlin.Double?) : Primitive
data class Double(val default: kotlin.Double?, override val description: kotlin.String?) :
Primitive

data class Boolean(val default: kotlin.Boolean?) : Primitive
data class Boolean(val default: kotlin.Boolean?, override val description: kotlin.String?) :
Primitive

data class String(val default: kotlin.String?) : Primitive
data class String(val default: kotlin.String?, override val description: kotlin.String?) :
Primitive

data object Unit : Primitive
data class Unit(override val description: kotlin.String?) : Primitive

fun default(): kotlin.String? =
when (this) {
Expand All @@ -146,32 +164,35 @@ sealed interface Model {
}
}

data object Binary : Model
data class OctetStream(override val description: String?) : Model

data object FreeFormJson : Model
data class FreeFormJson(override val description: String?) : Model

sealed interface Collection : Model {
val inner: Resolved<Model>

data class List(
override val inner: Resolved<Model>,
val default: kotlin.collections.List<String>?
val default: kotlin.collections.List<String>?,
override val description: String?
) : Collection

data class Set(
override val inner: Resolved<Model>,
val default: kotlin.collections.List<String>?
val default: kotlin.collections.List<String>?,
override val description: String?
) : Collection

data class Map(override val inner: Resolved<Model>) : Collection {
val key = Primitive.String(null)
data class Map(override val inner: Resolved<Model>, override val description: String?) :
Collection {
val key = Primitive.String(null, null)
}
}

@Serializable
data class Object(
val context: NamingContext,
val description: String?,
override val description: String?,
val properties: List<Property>
) : Model {
val inline: List<Model> =
Expand Down Expand Up @@ -206,7 +227,7 @@ sealed interface Model {
val context: NamingContext,
val cases: List<Case>,
val default: String?,
val description: String?
override val description: String?
) : Model {
val inline: List<Model> =
cases.mapNotNull {
Expand All @@ -229,7 +250,7 @@ sealed interface Model {
val context: NamingContext
val values: List<String>
val default: String?
val description: String?
override val description: String?

data class Closed(
override val context: NamingContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,10 +220,16 @@ private class OpenAPITransformer(private val openAPI: OpenAPI) {
properties != null -> toObject(context, properties!!)
additionalProperties != null ->
when (val aProps = additionalProperties!!) {
is AdditionalProperties.PSchema -> toMap(context, aProps)
is Allowed -> toRawJson(aProps)
is AdditionalProperties.PSchema ->
Collection.Map(aProps.value.resolve().toModel(context), description)
is Allowed ->
if (aProps.value) Model.FreeFormJson(description)
else
throw IllegalStateException(
"Illegal State: No additional properties allowed on empty object."
)
}
else -> toRawJson(Allowed(true))
else -> Model.FreeFormJson(description)
}

fun Schema.toObject(context: NamingContext, properties: Map<String, ReferenceOr<Schema>>): Model =
Expand Down Expand Up @@ -256,25 +262,21 @@ private class OpenAPITransformer(private val openAPI: OpenAPI) {
return Enum.Open(context, values, default, description)
}

fun toMap(context: NamingContext, additionalSchema: AdditionalProperties.PSchema): Model =
Collection.Map(additionalSchema.value.resolve().toModel(context))

fun toRawJson(allowed: Allowed): Model =
if (allowed.value) Model.FreeFormJson
else
throw IllegalStateException(
"Illegal State: No additional properties allowed on empty object."
)

fun Schema.toPrimitive(context: NamingContext, basic: Type.Basic): Model =
when (basic) {
Type.Basic.Object -> toRawJson(Allowed(true))
Type.Basic.Boolean -> Primitive.Boolean(default?.toString()?.toBoolean())
Type.Basic.Integer -> Primitive.Int(default?.toString()?.toIntOrNull())
Type.Basic.Number -> Primitive.Double(default?.toString()?.toDoubleOrNull())
Type.Basic.Object ->
if (Allowed(true).value) Model.FreeFormJson(description)
else
throw IllegalStateException(
"Illegal State: No additional properties allowed on empty object."
)
Type.Basic.Boolean -> Primitive.Boolean(default?.toString()?.toBoolean(), description)
Type.Basic.Integer -> Primitive.Int(default?.toString()?.toIntOrNull(), description)
Type.Basic.Number -> Primitive.Double(default?.toString()?.toDoubleOrNull(), description)
Type.Basic.Array -> collection(context)
Type.Basic.String ->
if (format == "binary") Model.Binary else Primitive.String(default?.toString())
if (format == "binary") Model.OctetStream(description)
else Primitive.String(default?.toString(), description)
Type.Basic.Null -> TODO("Schema.Type.Basic.Null")
}

Expand All @@ -294,8 +296,8 @@ private class OpenAPITransformer(private val openAPI: OpenAPI) {
}
null -> null
}
return if (uniqueItems == true) Collection.Set(inner, default)
else Collection.List(inner, default)
return if (uniqueItems == true) Collection.Set(inner, default, description)
else Collection.List(inner, default, description)
}

fun Schema.type(context: NamingContext, type: Type): Model =
Expand Down Expand Up @@ -472,9 +474,14 @@ private class OpenAPITransformer(private val openAPI: OpenAPI) {
}
}
.let(create)
Route.Body.Json.Defined(json.toModel(context), mediaType.extensions)

Route.Body.Json.Defined(
json.toModel(context),
body.description,
mediaType.extensions
)
}
?: Route.Body.Json.FreeForm(mediaType.extensions)
?: Route.Body.Json.FreeForm(body.description, mediaType.extensions)
Pair(ApplicationJson, json)
}
MultipartFormData.matches(contentType) -> {
Expand All @@ -499,22 +506,26 @@ private class OpenAPITransformer(private val openAPI: OpenAPI) {
when (resolved) {
is Resolved.Ref -> {
val model = resolved.toModel(Named(resolved.name)) as Resolved.Ref
Route.Body.Multipart.Ref(model, mediaType.extensions)
Route.Body.Multipart.Ref(model, body.description, mediaType.extensions)
}
is Resolved.Value ->
Route.Body.Multipart.Value(
resolved.value.properties!!.map { (name, ref) ->
val resolved = ref.resolve()
Route.Body.Multipart.FormData(name, resolved.toModel(ctx(name)))
},
body.description,
mediaType.extensions
)
}

Pair(MultipartFormData, multipart)
}
ApplicationOctetStream.matches(contentType) ->
Pair(ApplicationOctetStream, Route.Body.OctetStream(mediaType.extensions))
Pair(
ApplicationOctetStream,
Route.Body.OctetStream(body.description, mediaType.extensions)
)
else ->
throw IllegalStateException("RequestBody content type: $this not yet supported.")
}
Expand All @@ -534,7 +545,13 @@ private class OpenAPITransformer(private val openAPI: OpenAPI) {
val response = refOrResponse.get()
when {
response.content.contains("application/octet-stream") ->
Pair(statusCode, Route.ReturnType(Resolved.Value(Model.Binary), response.extensions))
Pair(
statusCode,
Route.ReturnType(
Resolved.Value(Model.OctetStream(response.description)),
response.extensions
)
)
response.content.contains("application/json") -> {
val mediaType = response.content.getValue("application/json")
val route =
Expand All @@ -553,14 +570,26 @@ private class OpenAPITransformer(private val openAPI: OpenAPI) {
Route.ReturnType(resolved.toModel(context), response.extensions)
}
null ->
Route.ReturnType(Resolved.Value(toRawJson(Allowed(true))), response.extensions)
Route.ReturnType(
Resolved.Value(
if (Allowed(true).value) Model.FreeFormJson(response.description)
else
throw IllegalStateException(
"Illegal State: No additional properties allowed on empty object."
)
),
response.extensions
)
}
Pair(statusCode, route)
}
response.isEmpty() ->
Pair(
statusCode,
Route.ReturnType(Resolved.Value(Primitive.String(null)), response.extensions)
Route.ReturnType(
Resolved.Value(Primitive.String(null, response.description)),
response.extensions
)
)
else ->
throw IllegalStateException("OpenAPI requires at least 1 valid response. $response")
Expand Down
22 changes: 18 additions & 4 deletions generation/src/jvmMain/kotlin/io/github/nomisrev/openapi/APIs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ private fun API.toImplementation(outerContext: NamingContext? = null): TypeSpec

private fun Route.toFun(implemented: Boolean): FunSpec =
FunSpec.builder(Nam.toParamName(Named(operation.operationId!!)))
.apply { operation.summary?.let { addKdoc(it) } }
.addModifiers(KModifier.SUSPEND, if (implemented) KModifier.OVERRIDE else KModifier.ABSTRACT)
.addParameters(params(defaults = !implemented))
.addParameters(requestBody(defaults = !implemented))
Expand Down Expand Up @@ -290,6 +291,7 @@ private fun Route.params(defaults: Boolean): List<ParameterSpec> =
input.type.toTypeName().copy(nullable = !input.isRequired)
)
.apply {
input.description?.let { addKdoc(it) }
if (defaults) {
defaultValue(input.type.value)
if (!input.isRequired && !input.type.value.hasDefault()) {
Expand All @@ -302,15 +304,27 @@ private fun Route.params(defaults: Boolean): List<ParameterSpec> =

// TODO support binary, and Xml
private fun Route.requestBody(defaults: Boolean): List<ParameterSpec> {
fun parameter(name: String, type: Resolved<Model>, nullable: Boolean): ParameterSpec =
fun parameter(
name: String,
type: Resolved<Model>,
nullable: Boolean,
description: String?
): ParameterSpec =
ParameterSpec.builder(name, type.toTypeName().copy(nullable = nullable))
.apply { if (defaults && nullable) defaultValue("null") }
.build()

return (body.jsonOrNull()?.let { json -> listOf(parameter("body", json.type, !body.required)) }
return (body.jsonOrNull()?.let { json ->
listOf(parameter("body", json.type, !body.required, json.description))
}
?: body.multipartOrNull()?.let { multipart ->
multipart.parameters.map { parameter ->
parameter(Nam.toParamName(Named(parameter.name)), parameter.type, !body.required)
parameter(
Nam.toParamName(Named(parameter.name)),
parameter.type,
!body.required,
parameter.type.value.description
)
}
})
.orEmpty()
Expand All @@ -321,7 +335,7 @@ private fun Route.returnType(): TypeName {
val success =
returnType.types.toSortedMap { s1, s2 -> s1.code.compareTo(s2.code) }.entries.first()
return when (success.value.type.value) {
is Model.Binary -> HttpResponse
is Model.OctetStream -> HttpResponse
else -> success.value.type.toTypeName()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ fun TypeName.nullable(): TypeName = copy(nullable = true)
inline fun <reified A : Annotation> annotationSpec(): AnnotationSpec =
AnnotationSpec.builder(A::class).build()

fun TypeSpec.Builder.description(kdoc: String?): TypeSpec.Builder = apply {
kdoc?.let { addKdoc("%L", it) }
}

fun ParameterSpec.Builder.description(kdoc: String?): ParameterSpec.Builder = apply {
kdoc?.let { addKdoc("%L", it) }
}

fun TypeSpec.Companion.dataClassBuilder(
className: ClassName,
parameters: List<ParameterSpec>
Expand Down
Loading

0 comments on commit e25bac6

Please sign in to comment.