Skip to content

Commit

Permalink
Improve TypeScriptEmitter for MWS integration
Browse files Browse the repository at this point in the history
  • Loading branch information
wilmveel committed Nov 22, 2023
1 parent 73c2f9d commit a98c807
Show file tree
Hide file tree
Showing 7 changed files with 76 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class CliTest {
}

@Test
fun testCliOpenapiKotlin() {
fun testCliOpenApiPetstoreKotlin() {
val packageName = "community.flock.openapi"
val packageDir = packageName.replace(".", "/")
val input = "${inputDir}/openapi/petstore.json"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,10 +259,10 @@ class KotlinEmitter(

}


fun Endpoint.Content.emitContentType() = type
.split("/", "-")
.joinToString("") { it.firstToUpper() }
.replace("+", "")

fun Type.Shape.Field.Reference.toField(identifier: String, isNullable: Boolean) = Type.Shape.Field(
Type.Shape.Field.Identifier(identifier),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ class TypeScriptEmitter(logger: Logger = noLogger) : AbstractEmitter(logger) {
override val shared = ""

private val endpointBase = """
|export namespace WirespecShared {
|${SPACER}type Method = "GET" | "PUT" | "POST" | "DELETE" | "OPTIONS" | "HEAD" | "PATCH" | "TRACE"
|${SPACER}type Content<T> = { type:string, body:T }
|export module Wirespec {
|${SPACER}export type Method = "GET" | "PUT" | "POST" | "DELETE" | "OPTIONS" | "HEAD" | "PATCH" | "TRACE"
|${SPACER}export type Content<T> = { type:string, body:T }
|${SPACER}export type Request<T> = { path:string, method: Method, query?: Record<string, any[]>, headers?: Record<string, any[]>, content?:Content<T> }
|${SPACER}export type Response<T> = { status:number, headers?: Record<string, any[]>, content?:Content<T> }
|${SPACER}export type Handler = (request:Request<any>) => Promise<Response<any>>
|}
""".trimMargin()

Expand Down Expand Up @@ -81,15 +82,19 @@ class TypeScriptEmitter(logger: Logger = noLogger) : AbstractEmitter(logger) {

override fun Endpoint.emit() = withLogging(logger) {
"""
|export namespace ${name} {
|export module ${name} {
|${SPACER}export const PATH = "/${path.joinToString ("/"){ when (it) {is Endpoint.Segment.Literal -> it.value; is Endpoint.Segment.Param -> ":${it.identifier.value}" } }}"
|${SPACER}export const METHOD = "${method.name}"
|${requests.toSet().joinToString("\n") { "${SPACER}type ${it.emitName()} = { path: ${path.emitType()}, method: \"${method}\", headers: {${headers.map { it.emit() }.joinToString(",")}}, query: {${query.map { it.emit() }.joinToString(",")}}${it.content?.let { ", content: { type: \"${it.type}\", body: ${it.reference.emit()} }" } ?: ""} } " }}
|${SPACER}export type Request = ${requests.toSet().joinToString(" | ") { it.emitName() }}
|${responses.toSet().joinToString("\n") { "${SPACER}type ${it.emitName()} = { status: ${if (it.status.isInt()) it.status else "number"}${it.content?.let { ", content: { type: \"${it.type}\", body: ${it.reference.emit()} }" } ?: ""} }" }}
|${responses.toSet().joinToString("\n") { "${SPACER}type ${it.emitName()} = { status: ${if (it.status.isInt()) it.status else "string"}${it.content?.let { ", content: { type: \"${it.type}\", body: ${it.reference.emit()} }" } ?: ""} }" }}
|${SPACER}export type Response = ${responses.toSet().joinToString(" | ") { it.emitName() }}
|${SPACER}export type Handler = (request:Request) => Promise<Response>
|${SPACER}export type Call = {
|${SPACER}${SPACER}${name.firstToLower()}:(request: Request) => Promise<Response>
|${SPACER}${SPACER}${name.firstToLower()}: Handler
|${SPACER}}
|${SPACER}${requests.joinToString("\n") { "export const ${it.emitName().firstToLower()} = (${joinParameters(it.content).joinToString(",") { it.emit() }}) => ({path: `${path.emitPath()}`, method: \"${method.name}\", query: {${query.emitMap()}}, headers: {${headers.emitMap()}}${it.content?.let { ", content: {type: \"${it.type}\", body}" } ?: ""}} as const)" }}
|${requests.joinToString("\n") { "${SPACER}export const ${it.emitName().firstToLower()} = (${joinParameters(it.content).takeIf { it.isNotEmpty() }?.joinToString(",", "obj:{", "}") { it.emit() }.orEmpty()}) => ({path: `${path.emitPath()}`, method: \"${method.name}\", query: {${query.emitMap()}}, headers: {${headers.emitMap()}}${it.content?.let { ", content: {type: \"${it.type}\", body: obj.body}" } ?: ""}} as const)" }}
|${responses.joinToString("\n") { "${SPACER}export const ${it.emitName().firstToLower()} = (${joinParameters(it.content).takeIf { it.isNotEmpty() }?.joinToString(",", "obj:{", "}") { it.emit() }.orEmpty()}) => ({status: ${if(it.status.isInt()) it.status else "`${it.status}`"}, headers: {${headers.emitMap()}}${it.content?.let { ", content: {type: \"${it.type}\", body: obj.body}" } ?: ""}} as const)" }}
|}
|
""".trimMargin()
Expand All @@ -106,13 +111,13 @@ class TypeScriptEmitter(logger: Logger = noLogger) : AbstractEmitter(logger) {
private fun Endpoint.Request.emitName() = "Request" + (content?.emitContentType() ?: "Undefined")
private fun Endpoint.Response.emitName() = "Response" + status.firstToUpper() + (content?.emitContentType() ?: "Undefined")

private fun List<Type.Shape.Field>.emitMap() = joinToString(", ") { "\"${it.identifier.emit()}\": ${it.identifier.emit()}" }
private fun List<Type.Shape.Field>.emitMap() = joinToString(", ") { "\"${it.identifier.emit()}\": obj.${it.identifier.emit()}" }

private fun List<Endpoint.Segment>.emitPath() = "/" + joinToString("/") { it.emit() }
private fun Endpoint.Segment.emit(): String = withLogging(logger) {
when (this) {
is Endpoint.Segment.Literal -> value
is Endpoint.Segment.Param -> "\${${identifier.value}}"
is Endpoint.Segment.Param -> "\${obj.${identifier.value}}"
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/compiler/lib/src/jsMain/kotlin/CompilationResult.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ class WsCompilationResult(
@JsExport
@ExperimentalJsExport
class WsCompiled(val value: String)

@JsExport
@ExperimentalJsExport
class WsCompiledFile(val name: String, val value: String)
22 changes: 19 additions & 3 deletions src/compiler/lib/src/jsMain/kotlin/Compiler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,32 @@ class WsToWirespec : Compiler() {
}
}

interface ParserInterface {
fun parse(source: String):Array<WsNode>
}

@JsExport
@ExperimentalJsExport
class OpenApiV2 {
object OpenApiV2Parser{
fun parse(source: String):Array<WsNode> = OpenApiParserV2.parse(source).produce()

}

@JsExport
@ExperimentalJsExport
class OpenApiV3 {
object OpenApiV3Parser{
fun parse(source: String):Array<WsNode> = OpenApiParserV3.parse(source).produce()
}


@JsExport
@ExperimentalJsExport
object OpenApiV3ToTypescript {
val logger = object : Logger() {}
private val emitter = TypeScriptEmitter(logger)
fun compile (source: String): Array<WsCompiledFile> {
val ast = OpenApiParserV3.parse(source)
return emitter.emit(ast)
.map { (file, value) -> WsCompiledFile(file, value)}
.toTypedArray()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ import community.flock.wirespec.compiler.core.emit.common.AbstractEmitter.Compan

object Common {
fun className(vararg arg: String) = arg
.flatMap { it.split("-") }
.flatMap { it.split("-", "/") }
.joinToString("") { it.firstToUpper() }
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class OpenApiParser(private val openApi: OpenAPIObject) {
path.toOperationList().map { (method, operation) ->
val parameters = path.resolveParameters() + operation.resolveParameters()
val segments = key.toSegments(parameters)
val name = operation.toName(segments, method)
val name = operation.toName() ?: (key.toName() + method.name)
val query = parameters
.filter { it.`in` == ParameterLocation.QUERY }
.map { it.toField(className(name, "Parameter", it.name)) }
Expand Down Expand Up @@ -123,15 +123,12 @@ class OpenApiParser(private val openApi: OpenAPIObject) {

private fun parseParameters() = openApi.flatMapRequests { req ->
val parameters = req.pathItem.resolveParameters() + req.operation.resolveParameters()
val segments = req.path.toSegments(parameters)
val name = req.operation.toName(segments, req.method)
val name = req.operation.toName() ?: (req.path.toName() + req.method.name)
parameters.flatMap { parameter -> parameter.schema?.flatten(className(name, "Parameter", parameter.name)) ?: emptyList() }
}

private fun parseRequestBody() = openApi.flatMapRequests { req ->
val parameters = req.pathItem.resolveParameters() + req.operation.resolveParameters()
val segments = req.path.toSegments(parameters)
val name = req.operation.toName(segments, req.method)
val name = req.operation.toName() ?: (req.path.toName() + req.method.name)
req.operation.requestBody?.resolve()?.content.orEmpty()
.flatMap { (_, mediaObject) ->
when (val schema = mediaObject.schema) {
Expand All @@ -152,9 +149,7 @@ class OpenApiParser(private val openApi: OpenAPIObject) {
}

private fun parseResponseBody() = openApi.flatMapResponses { res ->
val parameters = res.pathItem.resolveParameters() + (res.operation.resolveParameters())
val segments = res.path.toSegments(parameters)
val name = res.operation.toName(segments, res.method)
val name = res.operation.toName() ?: (res.path.toName() + res.method.name)
when (val response = res.response) {
is ResponseObject -> {
response.content.orEmpty().flatMap { (_, mediaObject) ->
Expand Down Expand Up @@ -188,38 +183,42 @@ class OpenApiParser(private val openApi: OpenAPIObject) {
}
.flatMap { it.value.flatten(className(it.key)) }

private fun Path.toSegments(parameters: List<ParameterObject>) = value.split("/").drop(1).map { segment ->
val isParam = segment[0] == '{' && segment[segment.length - 1] == '}'
when {
isParam -> {
val param = segment.substring(1, segment.length - 1)
parameters
.find { it.name == param }
?.schema
?.resolve()
?.let { it.type?.toPrimitive() }
?.let {
Endpoint.Segment.Param(
Field.Identifier(param),
Primitive(it, false)
)
}
?: error(" Declared path parameter $param needs to be defined as a path parameter in path or operation level")
private fun String.isParam() = this[0] == '{' && this[length - 1] == '}'

private fun OperationObject.toName() = this.operationId?.let { className(it) }
private fun Path.toName(): String = value
.split("/")
.drop(1)
.filter { it.isNotBlank() }
.joinToString("") {
when (it.isParam()) {
true -> className(it.substring(1, it.length - 1))
false -> className(it)
}

else -> Endpoint.Segment.Literal(segment)
}
}

private fun OperationObject.toName(segments: List<Endpoint.Segment>, method: Endpoint.Method) =
operationId?.let { className(it) } ?: segments
.joinToString("") {
when (it) {
is Endpoint.Segment.Literal -> className(it.value)
is Endpoint.Segment.Param -> className(it.identifier.value)
private fun Path.toSegments(parameters: List<ParameterObject>) = value.split("/").drop(1).filter { it.isNotBlank() }.map { segment ->
when(segment.isParam()) {
true -> {
val param = segment.substring(1, segment.length - 1)
val name = toName()
parameters
.find { it.name == param }
?.schema
?.resolve()
?.toReference(className(name, "Parameter", param))
?.let {
Endpoint.Segment.Param(
Field.Identifier(param),
it
)
}
?: error(" Declared path parameter $param needs to be defined as a path parameter in path or operation level")
}
}
.let { it + method.name }

false -> Endpoint.Segment.Literal(segment)
}
}

private fun OperationObject.resolveParameters(): List<ParameterObject> = parameters
?.mapNotNull {
Expand Down

0 comments on commit a98c807

Please sign in to comment.