Skip to content

Commit dc76cba

Browse files
committed
Add schema for checking the export
Related-To #139
1 parent 316dc36 commit dc76cba

4 files changed

Lines changed: 456 additions & 0 deletions

File tree

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"$id": "https://skainet.ai/schemas/operator-doc/v1",
4+
"title": "SKaiNET Operator Documentation Schema",
5+
"description": "JSON schema for SKaiNET operator documentation generated by KSP processor",
6+
"type": "object",
7+
"properties": {
8+
"schema": {
9+
"type": "string",
10+
"format": "uri",
11+
"description": "Schema URI identifier",
12+
"const": "https://skainet.ai/schemas/operator-doc/v1"
13+
},
14+
"version": {
15+
"type": "string",
16+
"pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$",
17+
"description": "Semantic version of the framework"
18+
},
19+
"commit": {
20+
"type": "string",
21+
"pattern": "^[a-f0-9]{7,40}$|^unknown$",
22+
"description": "Git commit SHA or 'unknown'"
23+
},
24+
"timestamp": {
25+
"type": "string",
26+
"format": "date-time",
27+
"description": "ISO 8601 timestamp when documentation was generated"
28+
},
29+
"module": {
30+
"type": "string",
31+
"minLength": 1,
32+
"description": "Name of the module containing the operators"
33+
},
34+
"operators": {
35+
"type": "array",
36+
"items": {
37+
"$ref": "#/$defs/OperatorDoc"
38+
},
39+
"description": "Array of operator documentation objects"
40+
}
41+
},
42+
"required": ["schema", "version", "commit", "timestamp", "module", "operators"],
43+
"additionalProperties": false,
44+
"$defs": {
45+
"OperatorDoc": {
46+
"type": "object",
47+
"properties": {
48+
"name": {
49+
"type": "string",
50+
"minLength": 1,
51+
"description": "Name of the operator class"
52+
},
53+
"packageName": {
54+
"type": "string",
55+
"pattern": "^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$",
56+
"description": "Fully qualified package name"
57+
},
58+
"modality": {
59+
"type": "string",
60+
"enum": ["core", "vision", "nlp", "audio"],
61+
"description": "Modality category of the operator"
62+
},
63+
"functions": {
64+
"type": "array",
65+
"items": {
66+
"$ref": "#/$defs/FunctionDoc"
67+
},
68+
"description": "Array of function documentation objects"
69+
}
70+
},
71+
"required": ["name", "packageName", "modality", "functions"],
72+
"additionalProperties": false
73+
},
74+
"FunctionDoc": {
75+
"type": "object",
76+
"properties": {
77+
"name": {
78+
"type": "string",
79+
"minLength": 1,
80+
"description": "Name of the function"
81+
},
82+
"signature": {
83+
"type": "string",
84+
"minLength": 1,
85+
"description": "Full function signature string"
86+
},
87+
"parameters": {
88+
"type": "array",
89+
"items": {
90+
"$ref": "#/$defs/ParameterDoc"
91+
},
92+
"description": "Array of parameter documentation objects"
93+
},
94+
"returnType": {
95+
"type": "string",
96+
"minLength": 1,
97+
"description": "Return type of the function"
98+
},
99+
"statusByBackend": {
100+
"type": "object",
101+
"patternProperties": {
102+
"^[a-zA-Z][a-zA-Z0-9_]*$": {
103+
"type": "string",
104+
"enum": ["implemented", "not_implemented", "in_progress"],
105+
"description": "Implementation status for this backend"
106+
}
107+
},
108+
"additionalProperties": false,
109+
"description": "Map of backend names to implementation status"
110+
},
111+
"notes": {
112+
"type": "array",
113+
"items": {
114+
"$ref": "#/$defs/Note"
115+
},
116+
"description": "Array of notes associated with the function"
117+
}
118+
},
119+
"required": ["name", "signature", "parameters", "returnType", "statusByBackend", "notes"],
120+
"additionalProperties": false
121+
},
122+
"ParameterDoc": {
123+
"type": "object",
124+
"properties": {
125+
"name": {
126+
"type": "string",
127+
"minLength": 1,
128+
"description": "Name of the parameter"
129+
},
130+
"type": {
131+
"type": "string",
132+
"minLength": 1,
133+
"description": "Type of the parameter"
134+
},
135+
"description": {
136+
"type": "string",
137+
"description": "Optional description of the parameter"
138+
}
139+
},
140+
"required": ["name", "type"],
141+
"additionalProperties": false
142+
},
143+
"Note": {
144+
"type": "object",
145+
"properties": {
146+
"type": {
147+
"type": "string",
148+
"enum": ["owner", "issue"],
149+
"description": "Type of the note"
150+
},
151+
"backend": {
152+
"type": "string",
153+
"pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
154+
"description": "Backend this note applies to"
155+
},
156+
"content": {
157+
"type": "string",
158+
"minLength": 1,
159+
"description": "Content of the note"
160+
}
161+
},
162+
"required": ["type", "backend", "content"],
163+
"additionalProperties": false
164+
}
165+
}
166+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package org.mikrograd.diff.ksp
2+
3+
import java.io.File
4+
import kotlin.system.exitProcess
5+
6+
/**
7+
* Main entry point for schema validation task.
8+
*
9+
* This is executed by the Gradle validateOperatorSchema task to validate
10+
* generated operator.json files against the JSON schema.
11+
*/
12+
fun main(args: Array<String>) {
13+
if (args.isEmpty()) {
14+
println("Error: Build directory path required as argument")
15+
exitProcess(1)
16+
}
17+
18+
val buildDirPath = args[0]
19+
val buildDir = File(buildDirPath)
20+
21+
println("Starting schema validation for operator documentation...")
22+
println("Build directory: ${buildDir.absolutePath}")
23+
24+
val validationResults = SchemaValidator.validateBuildOutput(buildDir)
25+
26+
if (validationResults.isEmpty()) {
27+
println("Warning: No validation results returned")
28+
exitProcess(1)
29+
}
30+
31+
var hasErrors = false
32+
var totalFiles = 0
33+
var validFiles = 0
34+
35+
for (result in validationResults) {
36+
totalFiles++
37+
38+
if (result.result.isValid) {
39+
validFiles++
40+
println("✓ VALID: ${result.file.relativeTo(buildDir)}")
41+
} else {
42+
hasErrors = true
43+
println("✗ INVALID: ${result.file.relativeTo(buildDir)}")
44+
println(" Errors:")
45+
for (error in result.result.errors) {
46+
println(" - $error")
47+
}
48+
}
49+
}
50+
51+
println("\n" + "=".repeat(60))
52+
println("Schema Validation Summary")
53+
println("=".repeat(60))
54+
println("Total files validated: $totalFiles")
55+
println("Valid files: $validFiles")
56+
println("Invalid files: ${totalFiles - validFiles}")
57+
58+
if (hasErrors) {
59+
println("\n❌ Schema validation FAILED")
60+
println("Please fix the validation errors above and run again.")
61+
exitProcess(1)
62+
} else {
63+
println("\n✅ All operator documentation files are valid!")
64+
println("Schema validation PASSED")
65+
exitProcess(0)
66+
}
67+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package org.mikrograd.diff.ksp
2+
3+
import com.fasterxml.jackson.databind.JsonNode
4+
import com.fasterxml.jackson.databind.ObjectMapper
5+
import com.networknt.schema.JsonSchema
6+
import com.networknt.schema.JsonSchemaFactory
7+
import com.networknt.schema.SpecVersion
8+
import com.networknt.schema.ValidationMessage
9+
import java.io.File
10+
import java.io.InputStream
11+
12+
/**
13+
* Utility class for validating operator documentation JSON against the JSON schema.
14+
*/
15+
object SchemaValidator {
16+
17+
private val objectMapper = ObjectMapper()
18+
private val schemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012)
19+
20+
/**
21+
* Validates a JSON file against the operator documentation schema.
22+
*
23+
* @param jsonFile The JSON file to validate
24+
* @return ValidationResult containing success status and any errors
25+
*/
26+
fun validateFile(jsonFile: File): ValidationResult {
27+
return try {
28+
if (!jsonFile.exists()) {
29+
return ValidationResult(false, listOf("File does not exist: ${jsonFile.absolutePath}"))
30+
}
31+
32+
val jsonNode = objectMapper.readTree(jsonFile)
33+
validate(jsonNode)
34+
} catch (e: Exception) {
35+
ValidationResult(false, listOf("Error reading JSON file: ${e.message}"))
36+
}
37+
}
38+
39+
/**
40+
* Validates a JSON string against the operator documentation schema.
41+
*
42+
* @param jsonContent The JSON content as a string
43+
* @return ValidationResult containing success status and any errors
44+
*/
45+
fun validateContent(jsonContent: String): ValidationResult {
46+
return try {
47+
val jsonNode = objectMapper.readTree(jsonContent)
48+
validate(jsonNode)
49+
} catch (e: Exception) {
50+
ValidationResult(false, listOf("Error parsing JSON content: ${e.message}"))
51+
}
52+
}
53+
54+
/**
55+
* Validates a JsonNode against the operator documentation schema.
56+
*
57+
* @param jsonNode The JsonNode to validate
58+
* @return ValidationResult containing success status and any errors
59+
*/
60+
private fun validate(jsonNode: JsonNode): ValidationResult {
61+
return try {
62+
val schema = loadSchema()
63+
val errors = schema.validate(jsonNode)
64+
65+
if (errors.isEmpty()) {
66+
ValidationResult(true, emptyList())
67+
} else {
68+
val errorMessages = errors.map { error ->
69+
"${error.path}: ${error.message}"
70+
}
71+
ValidationResult(false, errorMessages)
72+
}
73+
} catch (e: Exception) {
74+
ValidationResult(false, listOf("Schema validation error: ${e.message}"))
75+
}
76+
}
77+
78+
/**
79+
* Loads the JSON schema from resources.
80+
*
81+
* @return JsonSchema instance
82+
*/
83+
private fun loadSchema(): JsonSchema {
84+
val schemaStream = getSchemaStream()
85+
?: throw IllegalStateException("Cannot find schema resource: schemas/operator-doc-schema-v1.json")
86+
87+
return schemaFactory.getSchema(schemaStream)
88+
}
89+
90+
/**
91+
* Gets the schema file as an InputStream from resources.
92+
*
93+
* @return InputStream for the schema file or null if not found
94+
*/
95+
private fun getSchemaStream(): InputStream? {
96+
return this::class.java.classLoader.getResourceAsStream("schemas/operator-doc-schema-v1.json")
97+
}
98+
99+
/**
100+
* Validates all operator.json files in the given directory recursively.
101+
*
102+
* @param buildDir The build directory to search for operator.json files
103+
* @return List of ValidationResult for each file found
104+
*/
105+
fun validateBuildOutput(buildDir: File): List<FileValidationResult> {
106+
val results = mutableListOf<FileValidationResult>()
107+
108+
if (!buildDir.exists()) {
109+
return listOf(FileValidationResult(buildDir, ValidationResult(false, listOf("Build directory does not exist"))))
110+
}
111+
112+
val operatorJsonFiles = buildDir.walkTopDown()
113+
.filter { it.isFile && it.name == "operators.json" }
114+
.toList()
115+
116+
if (operatorJsonFiles.isEmpty()) {
117+
return listOf(FileValidationResult(buildDir, ValidationResult(false, listOf("No operators.json files found in build directory"))))
118+
}
119+
120+
for (file in operatorJsonFiles) {
121+
val result = validateFile(file)
122+
results.add(FileValidationResult(file, result))
123+
}
124+
125+
return results
126+
}
127+
}
128+
129+
/**
130+
* Result of JSON schema validation.
131+
*
132+
* @param isValid Whether the validation passed
133+
* @param errors List of validation error messages
134+
*/
135+
data class ValidationResult(
136+
val isValid: Boolean,
137+
val errors: List<String>
138+
) {
139+
/**
140+
* Returns a formatted string of all errors.
141+
*/
142+
fun getErrorsAsString(): String {
143+
return errors.joinToString("\n")
144+
}
145+
}
146+
147+
/**
148+
* Result of validating a specific file.
149+
*
150+
* @param file The file that was validated
151+
* @param result The validation result
152+
*/
153+
data class FileValidationResult(
154+
val file: File,
155+
val result: ValidationResult
156+
)

0 commit comments

Comments
 (0)