Skip to content

Commit 226b621

Browse files
dwursteisenclaude
andcommitted
Replace JSON string concatenation with a DSL builder
Introduce JsonDsl.kt with JsonObject/JsonArray builders following the same @DslMarker pattern as AsciidocDsl.kt. Rewrite generateJson() to use the DSL, eliminating manual StringBuilder, comma tracking, and the jsonString() helper. Output is byte-identical. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7475717 commit 226b621

File tree

2 files changed

+140
-82
lines changed

2 files changed

+140
-82
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package com.github.minigdx.tiny.doc
2+
3+
@DslMarker
4+
annotation class JsonDslMarker
5+
6+
private sealed interface JsonEntry {
7+
fun render(indent: Int): String
8+
}
9+
10+
private class JsonValueEntry(val name: String, val value: String?) : JsonEntry {
11+
override fun render(indent: Int): String {
12+
val pad = " ".repeat(indent)
13+
val v = if (value != null) "\"${escapeJson(value)}\"" else "null"
14+
return "$pad\"$name\": $v"
15+
}
16+
}
17+
18+
private class JsonObjectEntry(val name: String, val obj: JsonObject) : JsonEntry {
19+
override fun render(indent: Int): String {
20+
val pad = " ".repeat(indent)
21+
return "$pad\"$name\": ${obj.generate(indent)}"
22+
}
23+
}
24+
25+
private class JsonArrayEntry(val name: String, val arr: JsonArray) : JsonEntry {
26+
override fun render(indent: Int): String {
27+
val pad = " ".repeat(indent)
28+
return "$pad\"$name\": ${arr.generate(indent)}"
29+
}
30+
}
31+
32+
private fun escapeJson(value: String): String {
33+
return value
34+
.replace("\\", "\\\\")
35+
.replace("\"", "\\\"")
36+
.replace("\n", "\\n")
37+
.replace("\r", "\\r")
38+
.replace("\t", "\\t")
39+
}
40+
41+
@JsonDslMarker
42+
class JsonObject {
43+
private val entries = mutableListOf<JsonEntry>()
44+
45+
fun value(name: String, value: String?) {
46+
entries.add(JsonValueEntry(name, value))
47+
}
48+
49+
fun obj(name: String, block: JsonObject.() -> Unit) {
50+
val child = JsonObject()
51+
child.block()
52+
entries.add(JsonObjectEntry(name, child))
53+
}
54+
55+
fun array(name: String, block: JsonArray.() -> Unit) {
56+
val child = JsonArray()
57+
child.block()
58+
entries.add(JsonArrayEntry(name, child))
59+
}
60+
61+
fun generate(indent: Int = 0): String {
62+
val pad = " ".repeat(indent)
63+
val innerPad = " ".repeat(indent + 2)
64+
if (entries.isEmpty()) {
65+
return "{\n$pad}"
66+
}
67+
val content = entries.joinToString(",\n") { it.render(indent + 2) }
68+
return "{\n$content\n$pad}"
69+
}
70+
}
71+
72+
@JsonDslMarker
73+
class JsonArray {
74+
private val elements = mutableListOf<JsonObject>()
75+
76+
fun obj(block: JsonObject.() -> Unit) {
77+
val child = JsonObject()
78+
child.block()
79+
elements.add(child)
80+
}
81+
82+
fun generate(indent: Int = 0): String {
83+
val pad = " ".repeat(indent)
84+
val innerIndent = indent + 2
85+
if (elements.isEmpty()) {
86+
return "[\n$pad]"
87+
}
88+
val innerPad = " ".repeat(innerIndent)
89+
val content = elements.joinToString(",\n") { "$innerPad${it.generate(innerIndent)}" }
90+
return "[\n$content\n$pad]"
91+
}
92+
}
93+
94+
fun json(block: JsonObject.() -> Unit): JsonObject {
95+
val root = JsonObject()
96+
root.block()
97+
return root
98+
}

tiny-annotation-processors/tiny-api-to-json-generator/src/jvmMain/kotlin/com/github/minigdx/tiny/doc/TinyToJsonKspProcessor.kt

Lines changed: 42 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -42,92 +42,52 @@ class TinyToJsonKspProcessor(
4242
}
4343

4444
private fun generateJson(libs: Sequence<TinyLibDescriptor>): String {
45-
val sb = StringBuilder()
46-
sb.append("{\n")
47-
sb.append(" \"libraries\": [\n")
48-
4945
val sortedLibs = libs.sortedBy { it.name }.toList()
50-
sortedLibs.forEachIndexed { libIndex, lib ->
51-
sb.append(" {\n")
52-
sb.append(" \"name\": ${jsonString(lib.name.ifBlank { "std" })},\n")
53-
sb.append(" \"description\": ${jsonString(lib.description)},\n")
54-
sb.append(" \"icon\": ${jsonString(lib.icon)},\n")
55-
56-
// Variables
57-
sb.append(" \"variables\": [\n")
58-
val visibleVars = lib.variables.filterNot { it.hidden }.sortedBy { it.name }
59-
visibleVars.forEachIndexed { varIndex, variable ->
60-
sb.append(" {\n")
61-
sb.append(" \"name\": ${jsonString(variable.name)},\n")
62-
sb.append(" \"description\": ${jsonString(variable.description)}\n")
63-
sb.append(" }")
64-
if (varIndex < visibleVars.size - 1) sb.append(",")
65-
sb.append("\n")
66-
}
67-
sb.append(" ],\n")
68-
69-
// Functions
70-
sb.append(" \"functions\": [\n")
71-
val sortedFunctions = lib.functions.sortedBy { it.name }
72-
sortedFunctions.forEachIndexed { funcIndex, func ->
73-
sb.append(" {\n")
74-
sb.append(" \"name\": ${jsonString(func.name)},\n")
75-
sb.append(" \"description\": ${jsonString(func.description)},\n")
76-
77-
// Example
78-
if (func.example != null) {
79-
sb.append(" \"example\": ${jsonString(func.example!!)},\n")
80-
} else {
81-
sb.append(" \"example\": null,\n")
82-
}
83-
84-
// Calls (overloads)
85-
sb.append(" \"calls\": [\n")
86-
func.calls.forEachIndexed { callIndex, call ->
87-
sb.append(" {\n")
88-
sb.append(" \"description\": ${jsonString(call.description)},\n")
89-
sb.append(" \"returnType\": ${jsonString(call.returnType)},\n")
90-
sb.append(" \"args\": [\n")
91-
call.args.forEachIndexed { argIndex, arg ->
92-
sb.append(" {\n")
93-
sb.append(" \"name\": ${jsonString(arg.name)},\n")
94-
sb.append(" \"type\": ${jsonString(arg.type)},\n")
95-
sb.append(" \"description\": ${jsonString(arg.description)}\n")
96-
sb.append(" }")
97-
if (argIndex < call.args.size - 1) sb.append(",")
98-
sb.append("\n")
46+
return json {
47+
array("libraries") {
48+
sortedLibs.forEach { lib ->
49+
obj {
50+
value("name", lib.name.ifBlank { "std" })
51+
value("description", lib.description)
52+
value("icon", lib.icon)
53+
array("variables") {
54+
lib.variables.filterNot { it.hidden }.sortedBy { it.name }.forEach { variable ->
55+
obj {
56+
value("name", variable.name)
57+
value("description", variable.description)
58+
}
59+
}
60+
}
61+
array("functions") {
62+
lib.functions.sortedBy { it.name }.forEach { func ->
63+
obj {
64+
value("name", func.name)
65+
value("description", func.description)
66+
value("example", func.example)
67+
array("calls") {
68+
func.calls.forEach { call ->
69+
obj {
70+
value("description", call.description)
71+
value("returnType", call.returnType)
72+
array("args") {
73+
call.args.forEach { arg ->
74+
obj {
75+
value("name", arg.name)
76+
value("type", arg.type)
77+
value("description", arg.description)
78+
}
79+
}
80+
}
81+
}
82+
}
83+
}
84+
}
85+
}
86+
}
9987
}
100-
sb.append(" ]\n")
101-
sb.append(" }")
102-
if (callIndex < func.calls.size - 1) sb.append(",")
103-
sb.append("\n")
10488
}
105-
sb.append(" ]\n")
106-
107-
sb.append(" }")
108-
if (funcIndex < sortedFunctions.size - 1) sb.append(",")
109-
sb.append("\n")
11089
}
111-
sb.append(" ]\n")
112-
113-
sb.append(" }")
114-
if (libIndex < sortedLibs.size - 1) sb.append(",")
115-
sb.append("\n")
116-
}
117-
118-
sb.append(" ]\n")
119-
sb.append("}\n")
120-
return sb.toString()
121-
}
122-
123-
private fun jsonString(value: String): String {
124-
val escaped = value
125-
.replace("\\", "\\\\")
126-
.replace("\"", "\\\"")
127-
.replace("\n", "\\n")
128-
.replace("\r", "\\r")
129-
.replace("\t", "\\t")
130-
return "\"$escaped\""
90+
}.generate() + "\n"
13191
}
13292
}
13393

0 commit comments

Comments
 (0)