Skip to content

Commit

Permalink
Allow method filtering for generated APIs.
Browse files Browse the repository at this point in the history
This allows the :bluesky artifact to separate out official spec methods
from the "unspecced" group, isolating them to a different API class.
  • Loading branch information
christiandeange committed Nov 11, 2024
1 parent a82917f commit fabddfc
Show file tree
Hide file tree
Showing 6 changed files with 482 additions and 48 deletions.
393 changes: 393 additions & 0 deletions bluesky/api/bluesky.api

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions bluesky/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,17 @@ dependencies {

lexicons {
generateApi("BlueskyApi") {
packageName.set("sh.christian.ozone")
withKtorImplementation("XrpcBlueskyApi")
returnType.set(ApiReturnType.Response)
exclude("""app\.bsky\.unspecced\..*""")
}

generateApi("UnspeccedBlueskyApi") {
packageName.set("sh.christian.ozone.unspecced")
withKtorImplementation("XrpcUnspeccedBlueskyApi")
returnType.set(ApiReturnType.Response)
include("""app\.bsky\.unspecced\..*""")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ data class ApiConfiguration(
val implementationName: String?,
val suspending: Boolean,
val returnType: ApiReturnType,
val includeMethods: List<Regex>,
val excludeMethods: List<Regex>,
) : Serializable
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ class LexiconApiGenerator(
) {
val interfaceType = TypeSpec.interfaceBuilder(interfaceName)
.apply {
apiCalls.sortedBy { it.name }.forEach { apiCall ->
matchingApiCalls(configuration).forEach { apiCall ->
addFunction(apiCall.toFunctionSpec(configuration.returnType) {
if (configuration.suspending) {
addModifiers(KModifier.SUSPEND)
Expand Down Expand Up @@ -223,70 +223,72 @@ class LexiconApiGenerator(
.build()
)
.apply {
apiCalls.sortedBy { it.name }.forEach { apiCall ->
matchingApiCalls(configuration).forEach { apiCall ->
addFunction(apiCall.toFunctionSpec(configuration.returnType) {
if (configuration.suspending) {
addModifiers(KModifier.SUSPEND)
}

val code = buildCodeBlock {
val methodName = when (apiCall) {
is Query -> query
is Procedure -> procedure
is Subscription -> subscription
}
val path = "/xrpc/${apiCall.id}"
val transformingMethodName = when (configuration.returnType) {
ApiReturnType.Raw -> toAtpModel
ApiReturnType.Result -> toAtpResult
ApiReturnType.Response -> toAtpResponse
}
val methodName = when (apiCall) {
is Query -> query
is Procedure -> procedure
is Subscription -> subscription
}
val path = "/xrpc/${apiCall.id}"
val transformingMethodName = when (configuration.returnType) {
ApiReturnType.Raw -> toAtpModel
ApiReturnType.Result -> toAtpResult
ApiReturnType.Response -> toAtpResponse
}

// Workaround to prevent expression methods from being generated.
add("%L", "")
// Workaround to prevent expression methods from being generated.
add("%L", "")

if (configuration.suspending) {
add("return client.%M(\n", methodName)
} else {
beginControlFlow("return %M {", runBlocking)
add("client.%M(\n", methodName)
}
if (configuration.suspending) {
add("return client.%M(\n", methodName)
} else {
beginControlFlow("return %M {", runBlocking)
add("client.%M(\n", methodName)
}

indent()
add("path = %S,\n", path)
indent()
add("path = %S,\n", path)

when (apiCall) {
is Query -> {
if (apiCall.propertiesType != null) {
add("queryParams = params.asList(),\n")
}
}
is Procedure -> {
if (apiCall.inputType != null) {
add("body = request,\n")
add("encoding = %S,\n", apiCall.inputType.encoding)
}
}
is Subscription -> {
if (apiCall.propertiesType != null) {
add("queryParams = params.asList(),\n")
}
when (apiCall) {
is Query -> {
if (apiCall.propertiesType != null) {
add("queryParams = params.asList(),\n")
}
}

unindent()
when (apiCall) {
is Query -> add(").%M()", transformingMethodName)
is Procedure -> add(").%M()", transformingMethodName)
is Subscription -> add(").%M(::%M)", transformingMethodName, findSubscriptionSerializer)
is Procedure -> {
if (apiCall.inputType != null) {
add("body = request,\n")
add("encoding = %S,\n", apiCall.inputType.encoding)
}
}

if (!configuration.suspending) {
add("\n")
endControlFlow()
is Subscription -> {
if (apiCall.propertiesType != null) {
add("queryParams = params.asList(),\n")
}
}
}

unindent()
when (apiCall) {
is Query -> add(").%M()", transformingMethodName)
is Procedure -> add(").%M()", transformingMethodName)
is Subscription -> add(").%M(::%M)", transformingMethodName, findSubscriptionSerializer)
}

if (!configuration.suspending) {
add("\n")
endControlFlow()
}
}

addModifiers(KModifier.OVERRIDE)
addCode(code)
})
Expand All @@ -305,6 +307,13 @@ class LexiconApiGenerator(
ApiReturnType.Response -> AtpResponse.parameterizedBy(typeName)
}

private fun matchingApiCalls(configuration: ApiConfiguration): List<ApiCall> {
return apiCalls
.filter { api -> configuration.includeMethods.any { regex -> api.id.matches(regex) } }
.filterNot { api -> configuration.excludeMethods.any { regex -> api.id.matches(regex) } }
.sortedBy { it.name }
}

private sealed interface ApiCall {
val id: String
val name: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.kotlin.dsl.listProperty
import org.gradle.kotlin.dsl.property
import org.intellij.lang.annotations.Language
import sh.christian.ozone.api.generator.ApiConfiguration
import sh.christian.ozone.api.generator.ApiReturnType
import sh.christian.ozone.api.generator.ApiReturnType.Raw
Expand Down Expand Up @@ -54,6 +55,20 @@ abstract class LexiconGeneratorExtension
val returnType: Property<ApiReturnType> =
objects.property<ApiReturnType>().convention(Raw)

private val includeMethods: ListProperty<Regex> =
objects.listProperty<Regex>().convention(listOf(".*".toRegex()))

private val excludeMethods: ListProperty<Regex> =
objects.listProperty<Regex>().convention(emptyList())

fun include(@Language("RegExp") pattern: String) {
includeMethods.add(pattern.toRegex())
}

fun exclude(@Language("RegExp") pattern: String) {
excludeMethods.add(pattern.toRegex())
}

fun withKtorImplementation(name: String) {
implementationName.set(name)
}
Expand All @@ -65,6 +80,8 @@ abstract class LexiconGeneratorExtension
implementationName = implementationName.readFinalizedValueOrNull(),
suspending = suspending.readFinalizedValue(),
returnType = returnType.readFinalizedValue(),
includeMethods = includeMethods.readFinalizedValue(),
excludeMethods = excludeMethods.readFinalizedValue(),
)
}
}
Expand All @@ -79,3 +96,8 @@ private fun <T> Property<T>.readFinalizedValueOrNull(): T? {
finalizeValue()
return orNull
}

private fun <T> ListProperty<T>.readFinalizedValue(): List<T> {
finalizeValue()
return get()
}
1 change: 0 additions & 1 deletion lexicons/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,5 @@ plugins {
tasks.jar.configure {
from(fileTree("schemas") {
include("**/*.json")
exclude("**/unspecced/**")
})
}

0 comments on commit fabddfc

Please sign in to comment.