Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions core/src/main/kotlin/com/avsystem/justworks/core/gen/NameUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.avsystem.justworks.core.gen

private val DELIMITERS = Regex("[_\\-.]+")
private val CAMEL_BOUNDARY = Regex("(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])")

Comment thread
halotukozak marked this conversation as resolved.
Comment thread
halotukozak marked this conversation as resolved.
/**
* Converts a string to camelCase.
* Splits on `_`, `-`, `.` delimiters, lowercases first segment,
* capitalizes subsequent segments, and joins.
*/
fun String.toCamelCase(): String = toPascalCase().replaceFirstChar { it.lowercaseChar() }
Comment thread
halotukozak marked this conversation as resolved.

/**
* Converts a string to PascalCase.
* Like [toCamelCase] but capitalizes the first segment too.
*/
fun String.toPascalCase(): String = split(DELIMITERS)
Comment thread
halotukozak marked this conversation as resolved.
.filter { it.isNotEmpty() }
.flatMap { it.split(CAMEL_BOUNDARY) }
.joinToString("") { it.lowercase().replaceFirstChar { c -> c.uppercaseChar() } }

/**
* Converts any string to UPPER_SNAKE_CASE for use as an enum constant name.
* Inserts `_` before uppercase letters in camelCase, replaces non-alphanumeric
* with `_`, uppercases, deduplicates `_`, trims leading/trailing `_`.
*/
fun String.toEnumConstantName(): String {
val converted = replace(CAMEL_BOUNDARY, "_")
.replace(Regex("[^a-zA-Z0-9]+"), "_")
.trim('_')
.uppercase()

return when {
converted.isEmpty() -> this
Comment thread
halotukozak marked this conversation as resolved.
else -> converted
}
}

/**
* Generates a PascalCase operation name from HTTP method and path.
* Path parameters like {id} become "ById", {userId} becomes "ByUserId".
* Handles hyphens, underscores, and dots in path segments.
*
* Examples:
* - ("POST", "/pets") -> "PostPets"
* - ("GET", "/pets/{id}") -> "GetPetsById"
* - ("PUT", "/users/{userId}/orders/{orderId}") -> "PutUsersByUserIdOrdersByOrderId"
* - ("GET", "/api-tokens") -> "GetApiTokens"
*/
fun operationNameFromPath(method: String, path: String): String {
val methodPart = method.lowercase().replaceFirstChar { it.uppercase() }

val pathPart = path
.split("/")
.filter { it.isNotEmpty() }
.joinToString("") { segment ->
if (segment.startsWith("{") && segment.endsWith("}")) {
// Path parameter: {id} -> ById, {userId} -> ByUserId
val paramName = segment.removeSurrounding("{", "}")
"By" + paramName.toPascalCase()
} else {
segment.toPascalCase()
}
}

return methodPart + pathPart
}
81 changes: 81 additions & 0 deletions core/src/main/kotlin/com/avsystem/justworks/core/gen/Names.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.avsystem.justworks.core.gen

import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.MemberName

// ============================================================================
// Ktor HTTP Client
// ============================================================================

val HTTP_CLIENT = ClassName("io.ktor.client", "HttpClient")
val CONTENT_NEGOTIATION = ClassName("io.ktor.client.plugins.contentnegotiation", "ContentNegotiation")
val HTTP_HEADERS = ClassName("io.ktor.http", "HttpHeaders")

val JSON_FUN = MemberName("io.ktor.serialization.kotlinx.json", "json")
Comment thread
halotukozak marked this conversation as resolved.
val BODY_FUN = MemberName("io.ktor.client.call", "body")
val BODY_AS_TEXT_FUN = MemberName("io.ktor.client.statement", "bodyAsText")
Comment thread
halotukozak marked this conversation as resolved.
val SET_BODY_FUN = MemberName("io.ktor.client.request", "setBody")
val CONTENT_TYPE_FUN = MemberName("io.ktor.http", "contentType")
val CONTENT_TYPE_APPLICATION = ClassName("io.ktor.http", "ContentType", "Application")
val HEADERS_FUN = MemberName("io.ktor.client.request", "headers")

val GET_FUN = MemberName("io.ktor.client.request", "get")
val POST_FUN = MemberName("io.ktor.client.request", "post")
val PUT_FUN = MemberName("io.ktor.client.request", "put")
val DELETE_FUN = MemberName("io.ktor.client.request", "delete")
val PATCH_FUN = MemberName("io.ktor.client.request", "patch")

// ============================================================================
// kotlinx.serialization
// ============================================================================

val SERIALIZABLE = ClassName("kotlinx.serialization", "Serializable")
val SERIAL_NAME = ClassName("kotlinx.serialization", "SerialName")
val EXPERIMENTAL_SERIALIZATION_API = ClassName("kotlinx.serialization", "ExperimentalSerializationApi")
val SERIALIZATION_EXCEPTION = ClassName("kotlinx.serialization", "SerializationException")
val JSON_CLASS_DISCRIMINATOR = ClassName("kotlinx.serialization.json", "JsonClassDiscriminator")
val JSON_CLASS = ClassName("kotlinx.serialization.json", "Json")
val JSON_CONTENT_POLYMORPHIC_SERIALIZER = ClassName("kotlinx.serialization.json", "JsonContentPolymorphicSerializer")
val JSON_ELEMENT = ClassName("kotlinx.serialization.json", "JsonElement")
val SERIALIZERS_MODULE = ClassName("kotlinx.serialization.modules", "SerializersModule")

val JSON_OBJECT_EXT = MemberName("kotlinx.serialization.json", "jsonObject")

val ENCODE_TO_STRING_FUN = MemberName("kotlinx.serialization", "encodeToString")
val POLYMORPHIC_FUN = MemberName("kotlinx.serialization.modules", "polymorphic")
val SUBCLASS_FUN = MemberName("kotlinx.serialization.modules", "subclass")

// ============================================================================
// Date/Time (kotlinx.datetime)
// ============================================================================

val INSTANT = ClassName("kotlinx.datetime", "Instant")
val LOCAL_DATE = ClassName("kotlinx.datetime", "LocalDate")

// ============================================================================
// Error Handling (Arrow + Kotlin stdlib)
// ============================================================================

val RAISE = ClassName("arrow.core.raise", "Raise")
val RAISE_FUN = MemberName("arrow.core.raise.context", "raise")
val HTTP_ERROR = ClassName("com.avsystem.justworks", "HttpError")
val HTTP_ERROR_TYPE = ClassName("com.avsystem.justworks", "HttpErrorType")
val HTTP_SUCCESS = ClassName("com.avsystem.justworks", "HttpSuccess")

// ============================================================================
// Kotlin stdlib
// ============================================================================

val CLOSEABLE = ClassName("java.io", "Closeable")
val OPT_IN = ClassName("kotlin", "OptIn")

// ============================================================================
// Shared client base (generated)
// ============================================================================

val API_CLIENT_BASE = ClassName("com.avsystem.justworks", "ApiClientBase")
val HTTP_RESPONSE = ClassName("io.ktor.client.statement", "HttpResponse")
val HTTP_REQUEST_BUILDER = ClassName("io.ktor.client.request", "HttpRequestBuilder")
val TO_RESULT_FUN = MemberName("com.avsystem.justworks", "toResult")
val TO_EMPTY_RESULT_FUN = MemberName("com.avsystem.justworks", "toEmptyResult")
val ENCODE_PARAM_FUN = MemberName("com.avsystem.justworks", "encodeParam")
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.avsystem.justworks.core.gen

import com.avsystem.justworks.core.model.PrimitiveType
import com.avsystem.justworks.core.model.TypeRef
import com.squareup.kotlinpoet.ANY
import com.squareup.kotlinpoet.BOOLEAN
import com.squareup.kotlinpoet.BYTE_ARRAY
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.DOUBLE
import com.squareup.kotlinpoet.FLOAT
import com.squareup.kotlinpoet.INT
import com.squareup.kotlinpoet.LIST
import com.squareup.kotlinpoet.LONG
import com.squareup.kotlinpoet.MAP
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.STRING
import com.squareup.kotlinpoet.TypeName

/**
* Maps [TypeRef] sealed variants to KotlinPoet [TypeName] instances.
*/
object TypeMapping {
fun toTypeName(typeRef: TypeRef, modelPackage: String): TypeName = when (typeRef) {
is TypeRef.Primitive -> {
when (typeRef.type) {
PrimitiveType.STRING -> STRING
PrimitiveType.INT -> INT
PrimitiveType.LONG -> LONG
PrimitiveType.DOUBLE -> DOUBLE
PrimitiveType.FLOAT -> FLOAT
PrimitiveType.BOOLEAN -> BOOLEAN
PrimitiveType.BYTE_ARRAY -> BYTE_ARRAY
PrimitiveType.DATE_TIME -> INSTANT
PrimitiveType.DATE -> LOCAL_DATE
}
}

is TypeRef.Array -> {
LIST.parameterizedBy(toTypeName(typeRef.items, modelPackage))
}

is TypeRef.Map -> {
MAP.parameterizedBy(STRING, toTypeName(typeRef.valueType, modelPackage))
}

is TypeRef.Reference -> {
ClassName(modelPackage, typeRef.schemaName)
}

is TypeRef.Inline -> {
// For nested inline classes (e.g., "Pet.Address"), sanitize to "Pet_Address"
val sanitizedName = typeRef.contextHint.replace(".", "_")
ClassName(modelPackage, sanitizedName)
}

is TypeRef.Unknown -> {
ANY
}
}
}
Loading
Loading