Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interactive Playground #348

Merged
merged 20 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from 6 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
7 changes: 7 additions & 0 deletions playground/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM openjdk:17-jdk-slim

WORKDIR /app

COPY build/libs/playground-all.jar /app/playground-all.jar

CMD ["java", "-jar", "playground-all.jar"]
34 changes: 34 additions & 0 deletions playground/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Fabrikt Playground

The Fabrikt Playground is a web based tools that allows you to play around with
Fabrikt without installing it locally.

The goal is to make lower the barrier for trying out Fabrikt and to hopefully prove to
people that Fabrikt can be a useful tool for the user and encourage them to embed it in
their development workflow either via the CLI or via Gradle/Maven.

## Technical Details

The playground is built with these amazing Open Source libraries ♥️
* [Ktor](https://github.com/ktorio/ktor) for HTTP
* [kotlinx.html](https://github.com/Kotlin/kotlinx.html) for HTML without writing HTML
* [htmx](https://github.com/bigskysoftware/htmx) for interactivity
* [PrismJS](https://github.com/PrismJS/prism) for syntax highlighting
* [Ace Editor](https://github.com/ajaxorg/ace) for YAML editing
* [Normalize.css](https://github.com/necolas/normalize.css) for default styling
* [BassCSS](https://basscss.com/) for utility CSS

## Building and Deploying

1. Build the jar
```shell
gradle :playground:shadowJar
```

2. Build the Docker image

```shell
docker build -t fabrikt-playground .
```

3. Deploy to the PaaS of choice :rocket:
56 changes: 56 additions & 0 deletions playground/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
plugins {
application
kotlin("jvm")
id("com.github.johnrengelman.shadow") version "8.1.1"
}

application {
mainClass.set("PlaygroundApplicationKt")
applicationDefaultJvmArgs = listOf("-Dio.ktor.development=true")
}

repositories {
mavenCentral()
}

val ktorVersion: String by rootProject.extra

dependencies {
implementation(project(":"))

// ktor server
implementation("io.ktor:ktor-server-netty-jvm:$ktorVersion")
implementation("io.ktor:ktor-server-content-negotiation-jvm:$ktorVersion")
implementation("io.ktor:ktor-server-html-builder:$ktorVersion")
implementation("io.ktor:ktor-server-call-logging:$ktorVersion")

// logging
implementation("ch.qos.logback:logback-classic:1.5.6")

implementation("com.squareup:kotlinpoet:1.14.2") { exclude(module = "kotlin-stdlib-jre7") }

testImplementation(kotlin("test"))
}

tasks.test {
useJUnitPlatform()
}

kotlin {
jvmToolchain(17)
}

tasks.withType<Jar> {
manifest {
attributes["Main-Class"] = "PlaygroundApplicationKt"
}
}

tasks.withType<CreateStartScripts> {
dependsOn(tasks.shadowJar)
}

tasks.named("shadowJar") {
dependsOn(":shadowJar")
}

22 changes: 22 additions & 0 deletions playground/fly.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# fly.toml app configuration file generated for playground-green-sea-9337 on 2024-12-03T06:55:25+01:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#

app = 'playground-green-sea-9337'
primary_region = 'ams'

[build]

[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = 'suspend'
auto_start_machines = true
min_machines_running = 0
processes = ['app']

[[vm]]
memory = '512mb'
cpu_kind = 'shared'
cpus = 1
200 changes: 200 additions & 0 deletions playground/src/main/kotlin/PlaygroundApplication.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import com.cjbooms.fabrikt.cli.ClientCodeGenOptionType
import com.cjbooms.fabrikt.cli.ClientCodeGenTargetType
import com.cjbooms.fabrikt.cli.CodeGenTypeOverride
import com.cjbooms.fabrikt.cli.CodeGenerationType
import com.cjbooms.fabrikt.cli.ControllerCodeGenOptionType
import com.cjbooms.fabrikt.cli.ControllerCodeGenTargetType
import com.cjbooms.fabrikt.cli.ExternalReferencesResolutionMode
import com.cjbooms.fabrikt.cli.ModelCodeGenOptionType
import com.cjbooms.fabrikt.cli.SerializationLibrary
import com.cjbooms.fabrikt.cli.ValidationLibrary
import com.cjbooms.fabrikt.model.GeneratedFile
import com.cjbooms.fabrikt.model.KotlinSourceSet
import com.cjbooms.fabrikt.model.ResourceFile
import com.cjbooms.fabrikt.model.ResourceSourceSet
import com.cjbooms.fabrikt.model.SimpleFile
import data.sampleOpenApiSpec
import io.ktor.server.application.install
import io.ktor.server.engine.embeddedServer
import io.ktor.server.html.respondHtml
import io.ktor.server.http.content.staticResources
import io.ktor.server.netty.Netty
import io.ktor.server.plugins.calllogging.CallLogging
import io.ktor.server.request.receiveParameters
import io.ktor.server.response.respondText
import io.ktor.server.routing.get
import io.ktor.server.routing.post
import io.ktor.server.routing.routing
import kotlinx.html.a
import kotlinx.html.div
import kotlinx.html.h3
import kotlinx.html.p
import kotlinx.html.script
import kotlinx.html.stream.appendHTML
import kotlinx.html.style
import kotlinx.html.unsafe
import lib.generateCodeSynchronized
import views.elements.fileViewForFile
import views.elements.codeView
import views.elements.fileView
import views.elements.specForm
import views.layout.columnPanel
import views.layout.mainLayout
import views.respondHtmlFragmentDiv

fun main() {
embeddedServer(Netty, port = System.getenv("PORT")?.toIntOrNull() ?: 8080) {
install(CallLogging)

routing {
staticResources("/static", "static")

/**
* GET endpoint to render the playground
*/
get("/") {
call.respondHtml {
mainLayout {
columnPanel(
flexSizes = listOf(1.0, 1.0, 0.5),
// first column
{
specForm(sampleOpenApiSpec)
},
// second column
{
codeView { fileView("// Output will appear here") }
},
// third column
{
h3 {
style = "margin-top: 0;"
+"Happy with what you see?"
}
p {
+"Embed Fabrikt in your project and start generating code from OpenAPI specs today!"
}
p {
a(href = "https://github.com/cjbooms/fabrikt", target = "_blank") {
+"Fabrikt on GitHub"
}
}
}
)
}
}
}

/**
* POST endpoint to generate code from a spec
*
* Renders only the div containing the generated code.
*
* Loaded via AJAX with HTMX.
*/
post("/generate") {
val body = call.receiveParameters()

// validate input
val inputSpec = body["spec"]
if (inputSpec.isNullOrBlank()) {
return@post call.respondText {
buildString { appendHTML().div {
fileView("// Error: No spec provided")
script { unsafe { +"Prism.highlightAll();" } } // trigger syntax highlighting
} }
}
}

// parse input
val serializationLibraryInput = body["serializationLibrary"]
val serializationLibrary: SerializationLibrary? = if (serializationLibraryInput != null) {
SerializationLibrary.valueOf(serializationLibraryInput)
} else null

val genTypes: Set<CodeGenerationType> =
body.getAll("genTypes")?.map { CodeGenerationType.valueOf(it) }?.toSet()
?: emptySet()

val modelOptions: Set<ModelCodeGenOptionType> =
body.getAll("modelOptions")?.map { ModelCodeGenOptionType.valueOf(it) }?.toSet() ?: emptySet()

val controllerTargetInput = body["controllerTarget"]
val controllerTarget: ControllerCodeGenTargetType? = if (!controllerTargetInput.isNullOrBlank()) {
ControllerCodeGenTargetType.valueOf(controllerTargetInput)
} else null

val controllerOptions = body.getAll("controllerOptions")?.map{ ControllerCodeGenOptionType.valueOf(it) }?.toSet()
?: emptySet()

val modelSuffix = body["modelSuffix"]

val clientOptions = body.getAll("clientOptions")?.map { ClientCodeGenOptionType.valueOf(it) }?.toSet()
?: emptySet()

val clientTargetInput = body["clientTarget"]
val clientTarget = if (!clientTargetInput.isNullOrBlank()) {
ClientCodeGenTargetType.valueOf(clientTargetInput)
} else null

val typeOverrides = body.getAll("typeOverrides")?.map { CodeGenTypeOverride.valueOf(it) }?.toSet()
?: emptySet()

val validationLibraryInput = body["validationLibrary"]
val validationLibrary = if (!validationLibraryInput.isNullOrBlank()) {
ValidationLibrary.valueOf(validationLibraryInput)
} else null

val externalRefResolutionModeInput = body["externalRefResolutionMode"]
val externalRefResolutionMode = if (!externalRefResolutionModeInput.isNullOrBlank()) {
ExternalReferencesResolutionMode.valueOf(externalRefResolutionModeInput)
} else null

runCatching {
generateCodeSynchronized(
genTypes,
serializationLibrary,
modelOptions,
controllerTarget,
inputSpec,
controllerOptions,
modelSuffix,
clientOptions,
clientTarget,
typeOverrides,
validationLibrary,
externalRefResolutionMode,
)
}.onSuccess { generatedFiles ->
val fileNames = generatedFiles.fileNames()

call.respondHtmlFragmentDiv {
if (generatedFiles.isEmpty()) {
fileView("// No files generated. Try adjusting your settings.")
} else {
fileList(fileNames)
generatedFiles.forEach {
fileViewForFile(it)
}
}
script { unsafe { +"Prism.highlightAll();" } } // trigger syntax highlighting
}
}.onFailure { error ->
call.respondHtmlFragmentDiv {
fileView("// Error: ${error.message}")
script { unsafe { +"Prism.highlightAll();" } } // trigger syntax highlighting
}
}
}
}
}.start(wait = true)
}

private fun List<GeneratedFile>.fileNames(): List<String> = this.map { generatedFile ->
when (generatedFile) {
is KotlinSourceSet -> generatedFile.files.map { it.name }
is SimpleFile -> listOf(generatedFile.path.fileName.toString())
is ResourceFile -> listOf(generatedFile.fileName)
is ResourceSourceSet -> generatedFile.files.map { it.fileName }
}
}.flatten()
35 changes: 35 additions & 0 deletions playground/src/main/kotlin/data/SampleData.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package data

val sampleOpenApiSpec = """
openapi: 3.0.0
info:
title: Example API
version: 1.0.0
paths:
/hello:
get:
responses:
'200':
description: A simple hello world
content:
text/plain:
schema:
type: string
components:
schemas:
Pet:
type: object
required:
- name
- type
properties:
name:
type: string
type:
type: string
enum:
- dog
- cat
age:
type: integer
""".trimIndent()
Loading
Loading