Skip to content

Commit 4973b94

Browse files
committed
Add doc generator tool
Related-To #139
1 parent dc76cba commit 4973b94

5 files changed

Lines changed: 407 additions & 0 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: Schema Validation
2+
3+
on:
4+
push:
5+
branches: [ main, develop ]
6+
pull_request:
7+
branches: [ main, develop ]
8+
paths:
9+
- 'skainet-lang/**'
10+
- '.github/workflows/schema-validation.yml'
11+
12+
jobs:
13+
validate-schema:
14+
runs-on: ubuntu-latest
15+
16+
steps:
17+
- name: Checkout code
18+
uses: actions/checkout@v4
19+
20+
- name: Set up JDK 17
21+
uses: actions/setup-java@v4
22+
with:
23+
java-version: '17'
24+
distribution: 'temurin'
25+
26+
- name: Cache Gradle packages
27+
uses: actions/cache@v4
28+
with:
29+
path: |
30+
~/.gradle/caches
31+
~/.gradle/wrapper
32+
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
33+
restore-keys: |
34+
${{ runner.os }}-gradle-
35+
36+
- name: Grant execute permission for gradlew
37+
run: chmod +x gradlew
38+
39+
- name: Generate operator documentation
40+
run: ./gradlew :skainet-lang:skainet-lang-export-ops:kspKotlinJvm
41+
42+
- name: Validate JSON schema
43+
run: ./gradlew :skainet-lang:skainet-lang-export-ops:validateOperatorSchema
44+
45+
- name: Upload validation artifacts
46+
if: failure()
47+
uses: actions/upload-artifact@v4
48+
with:
49+
name: validation-logs
50+
path: |
51+
**/build/generated/**/*.json
52+
**/build/logs/
53+
retention-days: 7

tools/docgen/build.gradle.kts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
plugins {
2+
kotlin("jvm")
3+
alias(libs.plugins.kotlinSerialization)
4+
application
5+
}
6+
7+
dependencies {
8+
implementation(kotlin("stdlib"))
9+
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
10+
implementation("org.jetbrains.kotlinx:kotlinx-cli:0.3.6")
11+
12+
testImplementation(kotlin("test"))
13+
testImplementation("org.junit.jupiter:junit-jupiter:5.10.1")
14+
}
15+
16+
application {
17+
mainClass.set("sk.ainet.tools.docgen.DocGenKt")
18+
}
19+
20+
tasks.test {
21+
useJUnitPlatform()
22+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package sk.ainet.tools.docgen
2+
3+
import kotlinx.serialization.Serializable
4+
5+
@Serializable
6+
data class OperatorDocModule(
7+
val schema: String = "https://skainet.ai/schemas/operator-doc/v1",
8+
val version: String,
9+
val commit: String,
10+
val timestamp: String,
11+
val module: String,
12+
val operators: List<OperatorDoc>
13+
)
14+
15+
@Serializable
16+
data class OperatorDoc(
17+
val name: String,
18+
val packageName: String,
19+
val modality: String,
20+
val functions: List<FunctionDoc>
21+
)
22+
23+
@Serializable
24+
data class FunctionDoc(
25+
val name: String,
26+
val signature: String,
27+
val parameters: List<ParameterDoc>,
28+
val returnType: String,
29+
val statusByBackend: Map<String, String>,
30+
val notes: List<Note>
31+
)
32+
33+
@Serializable
34+
data class ParameterDoc(
35+
val name: String,
36+
val type: String,
37+
val description: String = ""
38+
)
39+
40+
@Serializable
41+
data class Note(
42+
val type: String,
43+
val backend: String,
44+
val content: String
45+
)
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
package sk.ainet.tools.docgen
2+
3+
import kotlinx.cli.*
4+
import kotlinx.serialization.json.Json
5+
import java.io.File
6+
import java.time.Instant
7+
import java.time.format.DateTimeFormatter
8+
9+
/**
10+
* Main documentation generator that converts JSON operator documentation to AsciiDoc format.
11+
*
12+
* Usage: DocGen -i input.json -o output_directory
13+
*/
14+
object DocGen {
15+
16+
private val json = Json {
17+
ignoreUnknownKeys = true
18+
prettyPrint = true
19+
}
20+
21+
fun generateDocumentation(inputFile: File, outputDir: File) {
22+
println("Reading JSON from: ${inputFile.absolutePath}")
23+
24+
val jsonContent = inputFile.readText()
25+
val module = json.decodeFromString<OperatorDocModule>(jsonContent)
26+
27+
println("Parsed module: ${module.module} with ${module.operators.size} operators")
28+
29+
// Create output directory structure
30+
outputDir.mkdirs()
31+
val generatedDir = File(outputDir, "_generated_")
32+
generatedDir.mkdirs()
33+
34+
// Generate main index page
35+
generateMainIndex(module, generatedDir)
36+
37+
// Generate individual operator pages
38+
module.operators.forEach { operator ->
39+
generateOperatorPage(operator, module, generatedDir)
40+
}
41+
42+
println("Generated documentation in: ${generatedDir.absolutePath}")
43+
}
44+
45+
private fun generateMainIndex(module: OperatorDocModule, outputDir: File) {
46+
val content = buildString {
47+
appendLine("= ${module.module} Operators")
48+
appendLine()
49+
appendLine("// Generated on ${formatTimestamp(module.timestamp)}")
50+
appendLine("// Version: ${module.version}")
51+
appendLine("// Commit: ${module.commit}")
52+
appendLine()
53+
appendLine("This documentation is automatically generated from the codebase annotations.")
54+
appendLine()
55+
appendLine("== Operators")
56+
appendLine()
57+
58+
// Group operators by modality
59+
val operatorsByModality = module.operators.groupBy { it.modality }
60+
operatorsByModality.entries.sortedBy { it.key }.forEach { (modality, operators) ->
61+
appendLine("=== ${modality.capitalize()} Operators")
62+
appendLine()
63+
operators.sortedBy { it.name }.forEach { operator ->
64+
appendLine("* xref:${operator.name.lowercase()}.adoc[${operator.name}] - ${operator.packageName}")
65+
}
66+
appendLine()
67+
}
68+
}
69+
70+
File(outputDir, "index.adoc").writeText(content)
71+
}
72+
73+
private fun generateOperatorPage(operator: OperatorDoc, module: OperatorDocModule, outputDir: File) {
74+
val content = buildString {
75+
appendLine("= ${operator.name}")
76+
appendLine()
77+
appendLine("// Generated on ${formatTimestamp(module.timestamp)}")
78+
appendLine("// Package: ${operator.packageName}")
79+
appendLine("// Modality: ${operator.modality}")
80+
appendLine()
81+
appendLine("Package: `${operator.packageName}`")
82+
appendLine()
83+
appendLine("Modality: *${operator.modality}*")
84+
appendLine()
85+
86+
if (operator.functions.isNotEmpty()) {
87+
appendLine("== Functions")
88+
appendLine()
89+
90+
operator.functions.sortedBy { it.name }.forEach { function ->
91+
generateFunctionSection(function, this)
92+
}
93+
}
94+
}
95+
96+
File(outputDir, "${operator.name.lowercase()}.adoc").writeText(content)
97+
}
98+
99+
private fun generateFunctionSection(function: FunctionDoc, builder: StringBuilder) {
100+
builder.apply {
101+
appendLine("=== ${function.name}")
102+
appendLine()
103+
appendLine("[source,kotlin]")
104+
appendLine("----")
105+
appendLine(function.signature)
106+
appendLine("----")
107+
appendLine()
108+
109+
// Parameters table
110+
if (function.parameters.isNotEmpty()) {
111+
appendLine("==== Parameters")
112+
appendLine()
113+
appendLine("[cols=\"1,2,3\"]")
114+
appendLine("|===")
115+
appendLine("| Name | Type | Description")
116+
appendLine()
117+
function.parameters.forEach { param ->
118+
appendLine("| ${param.name}")
119+
appendLine("| `${param.type}`")
120+
appendLine("| ${param.description.ifEmpty { "_No description_" }}")
121+
appendLine()
122+
}
123+
appendLine("|===")
124+
appendLine()
125+
}
126+
127+
// Return type
128+
appendLine("==== Returns")
129+
appendLine()
130+
appendLine("`${function.returnType}`")
131+
appendLine()
132+
133+
// Backend status table
134+
if (function.statusByBackend.isNotEmpty()) {
135+
appendLine("==== Backend Status")
136+
appendLine()
137+
generateBackendStatusTable(function, this)
138+
}
139+
140+
appendLine()
141+
}
142+
}
143+
144+
private fun generateBackendStatusTable(function: FunctionDoc, builder: StringBuilder) {
145+
builder.apply {
146+
appendLine("[cols=\"1,1,2\"]")
147+
appendLine("|===")
148+
appendLine("| Backend | Status | Notes")
149+
appendLine()
150+
151+
function.statusByBackend.entries.sortedBy { it.key }.forEach { (backend, status) ->
152+
appendLine("| ${backend}")
153+
appendLine("| ${formatStatus(status)}")
154+
155+
val backendNotes = function.notes.filter { it.backend == backend }
156+
if (backendNotes.isNotEmpty()) {
157+
val notesText = backendNotes.joinToString(", ") { note ->
158+
when (note.type) {
159+
"owner" -> "Owner: ${note.content}"
160+
"issue" -> "Issue: ${note.content}"
161+
else -> "${note.type}: ${note.content}"
162+
}
163+
}
164+
appendLine("| ${notesText}")
165+
} else {
166+
appendLine("| _None_")
167+
}
168+
appendLine()
169+
}
170+
appendLine("|===")
171+
appendLine()
172+
}
173+
}
174+
175+
private fun formatStatus(status: String): String {
176+
return when (status) {
177+
"implemented" -> "✅ Implemented"
178+
"not_implemented" -> "❌ Not Implemented"
179+
"in_progress" -> "🚧 In Progress"
180+
else -> status
181+
}
182+
}
183+
184+
private fun formatTimestamp(timestamp: String): String {
185+
return try {
186+
val instant = Instant.parse(timestamp)
187+
DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(instant.atZone(java.time.ZoneId.systemDefault()))
188+
} catch (e: Exception) {
189+
timestamp
190+
}
191+
}
192+
193+
private fun String.capitalize(): String {
194+
return this.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
195+
}
196+
}
197+
198+
fun main(args: Array<String>) {
199+
val parser = ArgParser("docgen")
200+
val input by parser.option(ArgType.String, shortName = "i", description = "Input JSON file").required()
201+
val output by parser.option(ArgType.String, shortName = "o", description = "Output directory").required()
202+
203+
parser.parse(args)
204+
205+
val inputFile = File(input)
206+
val outputDir = File(output)
207+
208+
if (!inputFile.exists()) {
209+
println("Error: Input file does not exist: $input")
210+
return
211+
}
212+
213+
DocGen.generateDocumentation(inputFile, outputDir)
214+
}

0 commit comments

Comments
 (0)