diff --git a/kson-lib-http/build.gradle.kts b/kson-lib-http/build.gradle.kts new file mode 100644 index 00000000..c5e3f5b9 --- /dev/null +++ b/kson-lib-http/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + kotlin("multiplatform") + kotlin("plugin.serialization") +} + +repositories { + mavenCentral() +} + +val ktorVersion = "3.4.0" + +kotlin { + jvm { + } + + sourceSets { + commonMain { + dependencies { + implementation("io.ktor:ktor-client-core:$ktorVersion") + implementation("io.ktor:ktor-client-cio:$ktorVersion") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1") + } + } + } +} + diff --git a/kson-lib-http/kson-api-schema.json b/kson-lib-http/kson-api-schema.json new file mode 100644 index 00000000..02cddad2 --- /dev/null +++ b/kson-lib-http/kson-api-schema.json @@ -0,0 +1,595 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://kson.org/api/schema", + "title": "KSON API Schema", + "description": "Schema for the KSON public API, supporting format, toJson, toYaml, analyze, parseSchema, validate, and validateEmbedRule commands.", + + "$defs": { + "Position": { + "type": "object", + "description": "A zero-based line/column position in a document.", + "properties": { + "line": { + "type": "integer", + "minimum": 0, + "description": "The line number (0-based)" + }, + "column": { + "type": "integer", + "minimum": 0, + "description": "The column number (0-based)" + } + }, + "required": ["line", "column"], + "additionalProperties": false + }, + + "Message": { + "type": "object", + "description": "A message logged during KSON processing.", + "properties": { + "message": { + "type": "string" + }, + "severity": { + "$ref": "#/$defs/MessageSeverity" + }, + "start": { + "$ref": "#/$defs/Position" + }, + "end": { + "$ref": "#/$defs/Position" + } + }, + "required": ["message", "severity", "start", "end"], + "additionalProperties": false + }, + + "MessageSeverity": { + "type": "string", + "enum": ["ERROR", "WARNING"] + }, + + "IndentType": { + "oneOf": [ + { + "type": "object", + "description": "Use spaces for indentation.", + "properties": { + "type": { "const": "spaces" }, + "size": { + "type": "integer", + "minimum": 1, + "default": 2, + "description": "Number of spaces per indent level" + } + }, + "required": ["type"], + "additionalProperties": false + }, + { + "type": "object", + "description": "Use tabs for indentation.", + "properties": { + "type": { "const": "tabs" } + }, + "required": ["type"], + "additionalProperties": false + } + ] + }, + + "FormattingStyle": { + "type": "string", + "enum": ["PLAIN", "DELIMITED", "COMPACT", "CLASSIC"] + }, + + "EmbedRule": { + "type": "object", + "description": "A rule for formatting string values at specific paths as embed blocks.", + "properties": { + "pathPattern": { + "type": "string", + "description": "A JsonPointerGlob pattern (e.g., \"/scripts/*\", \"/queries/**\")" + }, + "tag": { + "type": ["string", "null"], + "description": "Optional embed tag (e.g., \"yaml\", \"sql\", \"bash\")" + } + }, + "required": ["pathPattern"], + "additionalProperties": false + }, + + "FormatOptions": { + "type": "object", + "description": "Options for formatting KSON output.", + "properties": { + "indentType": { + "$ref": "#/$defs/IndentType" + }, + "formattingStyle": { + "$ref": "#/$defs/FormattingStyle" + }, + "embedBlockRules": { + "type": "array", + "items": { "$ref": "#/$defs/EmbedRule" } + } + }, + "additionalProperties": false + }, + + "TokenType": { + "type": "string", + "enum": [ + "CURLY_BRACE_L", + "CURLY_BRACE_R", + "SQUARE_BRACKET_L", + "SQUARE_BRACKET_R", + "ANGLE_BRACKET_L", + "ANGLE_BRACKET_R", + "COLON", + "DOT", + "END_DASH", + "COMMA", + "COMMENT", + "EMBED_OPEN_DELIM", + "EMBED_CLOSE_DELIM", + "EMBED_TAG", + "EMBED_PREAMBLE_NEWLINE", + "EMBED_CONTENT", + "FALSE", + "UNQUOTED_STRING", + "ILLEGAL_CHAR", + "LIST_DASH", + "NULL", + "NUMBER", + "STRING_OPEN_QUOTE", + "STRING_CLOSE_QUOTE", + "STRING_CONTENT", + "TRUE", + "WHITESPACE", + "EOF" + ] + }, + + "Token": { + "type": "object", + "description": "A token produced by the lexing phase.", + "properties": { + "tokenType": { "$ref": "#/$defs/TokenType" }, + "text": { "type": "string" }, + "start": { "$ref": "#/$defs/Position" }, + "end": { "$ref": "#/$defs/Position" } + }, + "required": ["tokenType", "text", "start", "end"], + "additionalProperties": false + }, + + "KsonValue": { + "oneOf": [ + { "$ref": "#/$defs/KsonObject" }, + { "$ref": "#/$defs/KsonArray" }, + { "$ref": "#/$defs/KsonString" }, + { "$ref": "#/$defs/KsonInteger" }, + { "$ref": "#/$defs/KsonDecimal" }, + { "$ref": "#/$defs/KsonBoolean" }, + { "$ref": "#/$defs/KsonNull" }, + { "$ref": "#/$defs/KsonEmbed" } + ] + }, + + "KsonObject": { + "type": "object", + "properties": { + "type": { "const": "OBJECT" }, + "properties": { + "type": "object", + "additionalProperties": { "$ref": "#/$defs/KsonValue" }, + "description": "Key-value pairs of the object" + }, + "propertyKeys": { + "type": "object", + "additionalProperties": { "$ref": "#/$defs/KsonString" }, + "description": "Property key metadata (including positions)" + }, + "start": { "$ref": "#/$defs/Position" }, + "end": { "$ref": "#/$defs/Position" } + }, + "required": ["type", "properties", "propertyKeys", "start", "end"], + "additionalProperties": false + }, + + "KsonArray": { + "type": "object", + "properties": { + "type": { "const": "ARRAY" }, + "elements": { + "type": "array", + "items": { "$ref": "#/$defs/KsonValue" } + }, + "start": { "$ref": "#/$defs/Position" }, + "end": { "$ref": "#/$defs/Position" } + }, + "required": ["type", "elements", "start", "end"], + "additionalProperties": false + }, + + "KsonString": { + "type": "object", + "properties": { + "type": { "const": "STRING" }, + "value": { "type": "string" }, + "start": { "$ref": "#/$defs/Position" }, + "end": { "$ref": "#/$defs/Position" } + }, + "required": ["type", "value", "start", "end"], + "additionalProperties": false + }, + + "KsonInteger": { + "type": "object", + "properties": { + "type": { "const": "INTEGER" }, + "value": { "type": "integer" }, + "start": { "$ref": "#/$defs/Position" }, + "end": { "$ref": "#/$defs/Position" } + }, + "required": ["type", "value", "start", "end"], + "additionalProperties": false + }, + + "KsonDecimal": { + "type": "object", + "properties": { + "type": { "const": "DECIMAL" }, + "value": { "type": "number" }, + "start": { "$ref": "#/$defs/Position" }, + "end": { "$ref": "#/$defs/Position" } + }, + "required": ["type", "value", "start", "end"], + "additionalProperties": false + }, + + "KsonBoolean": { + "type": "object", + "properties": { + "type": { "const": "BOOLEAN" }, + "value": { "type": "boolean" }, + "start": { "$ref": "#/$defs/Position" }, + "end": { "$ref": "#/$defs/Position" } + }, + "required": ["type", "value", "start", "end"], + "additionalProperties": false + }, + + "KsonNull": { + "type": "object", + "properties": { + "type": { "const": "NULL" }, + "start": { "$ref": "#/$defs/Position" }, + "end": { "$ref": "#/$defs/Position" } + }, + "required": ["type", "start", "end"], + "additionalProperties": false + }, + + "KsonEmbed": { + "type": "object", + "properties": { + "type": { "const": "EMBED" }, + "tag": { + "type": ["string", "null"], + "description": "Optional embed tag" + }, + "content": { "type": "string" }, + "start": { "$ref": "#/$defs/Position" }, + "end": { "$ref": "#/$defs/Position" } + }, + "required": ["type", "content", "start", "end"], + "additionalProperties": false + }, + + "FormatCommand": { + "type": "object", + "description": "Formats KSON source with the specified formatting options.", + "properties": { + "command": { "const": "format" }, + "kson": { + "type": "string", + "description": "The KSON source to format" + }, + "formatOptions": { + "$ref": "#/$defs/FormatOptions" + } + }, + "required": ["command", "kson"], + "additionalProperties": false + }, + + "ToJsonCommand": { + "type": "object", + "description": "Converts KSON to JSON.", + "properties": { + "command": { "const": "toJson" }, + "kson": { + "type": "string", + "description": "The KSON source to convert" + }, + "retainEmbedTags": { + "type": "boolean", + "default": true, + "description": "Whether to retain embed tags in the JSON output" + } + }, + "required": ["command", "kson"], + "additionalProperties": false + }, + + "ToYamlCommand": { + "type": "object", + "description": "Converts KSON to YAML, preserving comments.", + "properties": { + "command": { "const": "toYaml" }, + "kson": { + "type": "string", + "description": "The KSON source to convert" + }, + "retainEmbedTags": { + "type": "boolean", + "default": true, + "description": "Whether to retain embed tags in the YAML output" + } + }, + "required": ["command", "kson"], + "additionalProperties": false + }, + + "AnalyzeCommand": { + "type": "object", + "description": "Statically analyze KSON and return messages, tokens, and parsed value.", + "properties": { + "command": { "const": "analyze" }, + "kson": { + "type": "string", + "description": "The KSON source to analyze" + }, + "filepath": { + "type": ["string", "null"], + "description": "Optional filepath of the document being analyzed" + } + }, + "required": ["command", "kson"], + "additionalProperties": false + }, + + "ParseSchemaCommand": { + "type": "object", + "description": "Parses a KSON schema definition.", + "properties": { + "command": { "const": "parseSchema" }, + "schemaKson": { + "type": "string", + "description": "The KSON source defining a JSON Schema" + } + }, + "required": ["command", "schemaKson"], + "additionalProperties": false + }, + + "ValidateCommand": { + "type": "object", + "description": "Parses a schema and validates a KSON document against it in a single step.", + "properties": { + "command": { "const": "validate" }, + "schemaKson": { + "type": "string", + "description": "The KSON source defining a JSON Schema" + }, + "kson": { + "type": "string", + "description": "The KSON source to validate against the schema" + }, + "filepath": { + "type": ["string", "null"], + "description": "Optional filepath of the document being validated" + } + }, + "required": ["command", "schemaKson", "kson"], + "additionalProperties": false + }, + + "ValidateEmbedRuleCommand": { + "type": "object", + "description": "Validates that the given EmbedRule has a valid JsonPointerGlob pathPattern.", + "properties": { + "command": { "const": "validateEmbedRule" }, + "embedBlockRule": { + "$ref": "#/$defs/EmbedRule", + "description": "The embed rule to validate" + } + }, + "required": ["command", "embedBlockRule"], + "additionalProperties": false + }, + + "FormatResult": { + "type": "object", + "description": "Result of the format command.", + "properties": { + "command": { "const": "format" }, + "success": { "const": true }, + "output": { + "type": "string", + "description": "The formatted KSON source" + } + }, + "required": ["command", "success", "output"], + "additionalProperties": false + }, + + "TranspileSuccessResult": { + "type": "object", + "properties": { + "command": { + "type": "string", + "enum": ["toJson", "toYaml"] + }, + "success": { "const": true }, + "output": { + "type": "string", + "description": "The transpiled output (JSON or YAML)" + } + }, + "required": ["command", "success", "output"], + "additionalProperties": false + }, + + "TranspileFailureResult": { + "type": "object", + "properties": { + "command": { + "type": "string", + "enum": ["toJson", "toYaml"] + }, + "success": { "const": false }, + "errors": { + "type": "array", + "items": { "$ref": "#/$defs/Message" } + } + }, + "required": ["command", "success", "errors"], + "additionalProperties": false + }, + + "AnalyzeResult": { + "type": "object", + "description": "Result of the analyze command.", + "properties": { + "command": { "const": "analyze" }, + "errors": { + "type": "array", + "items": { "$ref": "#/$defs/Message" } + }, + "tokens": { + "type": "array", + "items": { "$ref": "#/$defs/Token" } + }, + "ksonValue": { + "oneOf": [ + { "$ref": "#/$defs/KsonValue" }, + { "type": "null" } + ] + } + }, + "required": ["command", "errors", "tokens", "ksonValue"], + "additionalProperties": false + }, + + "ParseSchemaSuccessResult": { + "type": "object", + "properties": { + "command": { "const": "parseSchema" }, + "success": { "const": true } + }, + "required": ["command", "success"], + "additionalProperties": false + }, + + "ParseSchemaFailureResult": { + "type": "object", + "properties": { + "command": { "const": "parseSchema" }, + "success": { "const": false }, + "errors": { + "type": "array", + "items": { "$ref": "#/$defs/Message" } + } + }, + "required": ["command", "success", "errors"], + "additionalProperties": false + }, + + "ValidateResult": { + "type": "object", + "description": "Result of the validate command.", + "properties": { + "command": { "const": "validate" }, + "success": { + "type": "boolean", + "description": "True if both schema parsing and validation succeeded with no errors" + }, + "errors": { + "type": "array", + "items": { "$ref": "#/$defs/Message" }, + "description": "Schema parsing errors or validation errors" + } + }, + "required": ["command", "success", "errors"], + "additionalProperties": false + }, + + "EmbedRuleSuccessResult": { + "type": "object", + "description": "Successful result of the validateEmbedRule command.", + "properties": { + "command": { "const": "validateEmbedRule" }, + "success": { "const": true } + }, + "required": ["command", "success"], + "additionalProperties": false + }, + + "EmbedRuleFailureResult": { + "type": "object", + "description": "Failed result of the validateEmbedRule command.", + "properties": { + "command": { "const": "validateEmbedRule" }, + "success": { "const": false }, + "error": { + "type": "string", + "description": "Error message for the invalid pathPattern" + } + }, + "required": ["command", "success", "error"], + "additionalProperties": false + } + }, + + "type": "object", + "properties": { + "request": { + "description": "A KSON API command request.", + "oneOf": [ + { "$ref": "#/$defs/FormatCommand" }, + { "$ref": "#/$defs/ToJsonCommand" }, + { "$ref": "#/$defs/ToYamlCommand" }, + { "$ref": "#/$defs/AnalyzeCommand" }, + { "$ref": "#/$defs/ParseSchemaCommand" }, + { "$ref": "#/$defs/ValidateCommand" }, + { "$ref": "#/$defs/ValidateEmbedRuleCommand" } + ], + "discriminator": { + "propertyName": "command" + } + }, + "response": { + "description": "A KSON API command response.", + "oneOf": [ + { "$ref": "#/$defs/FormatResult" }, + { "$ref": "#/$defs/TranspileSuccessResult" }, + { "$ref": "#/$defs/TranspileFailureResult" }, + { "$ref": "#/$defs/AnalyzeResult" }, + { "$ref": "#/$defs/ParseSchemaSuccessResult" }, + { "$ref": "#/$defs/ParseSchemaFailureResult" }, + { "$ref": "#/$defs/ValidateResult" }, + { "$ref": "#/$defs/EmbedRuleSuccessResult" }, + { "$ref": "#/$defs/EmbedRuleFailureResult" } + ], + "discriminator": { + "propertyName": "command" + } + } + }, + "additionalProperties": false +} diff --git a/kson-lib-http/readme.md b/kson-lib-http/readme.md new file mode 100644 index 00000000..c39a244b --- /dev/null +++ b/kson-lib-http/readme.md @@ -0,0 +1,24 @@ +# kson-lib-http + +A reimplementation of KSON's public API (see [kson-lib](../kson-lib)) based on +HTTP requests to a server that respects the [kson-api-schema](./kson-api-schema.json). +This code is meant to facilitate testing, so users of KSON should ignore it +and rely on `kson-lib` for all their KSON needs. We might implement a +user-facing, HTTP-based KSON API in the future. + +> [!WARNING] +> The JSON Schema is highly experimental, possibly inaccurate in some places, and +> subject to change without notice + +### Testing KSON implementations + +If you have an implementation of the KSON public API that you would like to +test, all you need to do is create an HTTP server that handles KSON requests +according to the [schema](./kson-api-schema.json) and internally dispatches +them to your actual KSON implementation. This sounds complicated, but a good +LLM can easily one-shot server creation based on the schema and your +implementation's source code. + +With that in place, you then need to instruct Gradle to run the `kson-lib` +tests against your server. For reference, see the [test project](../lib-python/kson-lib-tests/) +we use to run the `kson-lib` tests against our Python bindings. diff --git a/kson-lib-http/src/commonMain/kotlin/org/kson/Kson.kt b/kson-lib-http/src/commonMain/kotlin/org/kson/Kson.kt new file mode 100644 index 00000000..365b2f44 --- /dev/null +++ b/kson-lib-http/src/commonMain/kotlin/org/kson/Kson.kt @@ -0,0 +1,432 @@ +package org.kson + +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.* + +object Kson { + private val client = HttpClient(CIO) + private val json = Json { ignoreUnknownKeys = true } + private var baseUrl = "http://localhost:8080" + + fun setPort(port: Int) { + baseUrl = "http://localhost:$port" + } + + fun format(kson: String, formatOptions: FormatOptions = FormatOptions()): String { + val request = buildJsonObject { + put("command", "format") + put("kson", kson) + put("formatOptions", formatOptions.toJson()) + } + val response = post(request) + return response["output"]!!.jsonPrimitive.content + } + + + fun toJson(kson: String, options: TranspileOptions.Json = TranspileOptions.Json()): Result { + val request = buildJsonObject { + put("command", "toJson") + put("kson", kson) + put("retainEmbedTags", options.retainEmbedTags) + } + return parseTranspileResult(post(request)) + } + + + fun toYaml(kson: String, options: TranspileOptions.Yaml = TranspileOptions.Yaml()): Result { + val request = buildJsonObject { + put("command", "toYaml") + put("kson", kson) + put("retainEmbedTags", options.retainEmbedTags) + } + return parseTranspileResult(post(request)) + } + + + fun analyze(kson: String, filepath: String? = null): Analysis { + val request = buildJsonObject { + put("command", "analyze") + put("kson", kson) + filepath?.let { put("filepath", it) } + } + val response = post(request) + val errors = parseMessages(response["errors"]!!.jsonArray) + val tokens = parseTokens(response["tokens"]!!.jsonArray) + val ksonValue = response["ksonValue"]?.let { + if (it is JsonNull) null else parseKsonValue(it.jsonObject) + } + return Analysis(errors, tokens, ksonValue) + } + + + fun parseSchema(schemaKson: String): SchemaResult { + val request = buildJsonObject { + put("command", "parseSchema") + put("schemaKson", schemaKson) + } + val response = post(request) + val success = response["success"]!!.jsonPrimitive.boolean + return if (success) { + SchemaResult.Success(SchemaValidator(schemaKson)) + } else { + SchemaResult.Failure(parseMessages(response["errors"]!!.jsonArray)) + } + } + + internal fun post(body: JsonObject): JsonObject = runBlocking { + val response = client.post(baseUrl) { + contentType(ContentType.Application.Json) + setBody(body.toString()) + } + json.parseToJsonElement(response.bodyAsText()).jsonObject + } + + private fun parseTranspileResult(response: JsonObject): Result { + val success = response["success"]!!.jsonPrimitive.boolean + return if (success) { + Result.Success(response["output"]!!.jsonPrimitive.content) + } else { + Result.Failure(parseMessages(response["errors"]!!.jsonArray)) + } + } +} + + +sealed class Result { + class Success(val output: String) : Result() + class Failure(val errors: List) : Result() +} + + +sealed class SchemaResult { + class Success(val schemaValidator: SchemaValidator) : SchemaResult() + class Failure(val errors: List) : SchemaResult() +} + +class SchemaValidator internal constructor(private val schemaKson: String) { + + fun validate(kson: String, filepath: String? = null): List { + val request = buildJsonObject { + put("command", "validate") + put("schemaKson", schemaKson) + put("kson", kson) + filepath?.let { put("filepath", it) } + } + val response = Kson.post(request) + return parseMessages(response["errors"]!!.jsonArray) + } +} + + +class EmbedRule private constructor( + val pathPattern: String, + val tag: String? = null +) { + companion object { + fun fromPathPattern(pathPattern: String, tag: String? = null): EmbedRuleResult { + val request = buildJsonObject { + put("command", "validateEmbedRule") + put("embedBlockRule", buildJsonObject { + put("pathPattern", pathPattern) + tag?.let { put("tag", it) } + }) + } + val response = Kson.post(request) + val success = response["success"]?.jsonPrimitive?.boolean == true + return if (success) { + EmbedRuleResult.Success(EmbedRule(pathPattern, tag)) + } else { + val error = response["error"]!!.jsonPrimitive.content + EmbedRuleResult.Failure(error) + } + } + } +} + +sealed class EmbedRuleResult { + data class Success(val embedRule: EmbedRule) : EmbedRuleResult() + data class Failure(val message: String) : EmbedRuleResult() +} + + +class FormatOptions( + val indentType: IndentType = IndentType.Spaces(2), + val formattingStyle: FormattingStyle = FormattingStyle.PLAIN, + val embedBlockRules: List = emptyList() +) { + internal fun toJson(): JsonObject = buildJsonObject { + put("indentType", when (indentType) { + is IndentType.Spaces -> buildJsonObject { + put("type", "spaces") + put("size", indentType.size) + } + is IndentType.Tabs -> buildJsonObject { + put("type", "tabs") + } + }) + put("formattingStyle", formattingStyle.name) + put("embedBlockRules", buildJsonArray { + for (rule in embedBlockRules) { + add(buildJsonObject { + put("pathPattern", rule.pathPattern) + rule.tag?.let { put("tag", it) } + }) + } + }) + } +} + + +sealed class TranspileOptions { + abstract val retainEmbedTags: Boolean + + + class Json( + override val retainEmbedTags: Boolean = true + ) : TranspileOptions() + + + class Yaml( + override val retainEmbedTags: Boolean = true + ) : TranspileOptions() +} + + +enum class FormattingStyle { + PLAIN, + DELIMITED, + COMPACT, + CLASSIC +} + +sealed class IndentType { + + class Spaces(val size: Int = 2) : IndentType() + + + data object Tabs : IndentType() +} + + +class Analysis internal constructor( + val errors: List, + val tokens: List, + val ksonValue: KsonValue? +) + + +class Token internal constructor( + val tokenType: TokenType, + val text: String, + val start: Position, + val end: Position) + +enum class TokenType { + CURLY_BRACE_L, + CURLY_BRACE_R, + SQUARE_BRACKET_L, + SQUARE_BRACKET_R, + ANGLE_BRACKET_L, + ANGLE_BRACKET_R, + COLON, + DOT, + END_DASH, + COMMA, + COMMENT, + EMBED_OPEN_DELIM, + EMBED_CLOSE_DELIM, + EMBED_TAG, + EMBED_PREAMBLE_NEWLINE, + EMBED_CONTENT, + FALSE, + UNQUOTED_STRING, + ILLEGAL_CHAR, + LIST_DASH, + NULL, + NUMBER, + STRING_OPEN_QUOTE, + STRING_CLOSE_QUOTE, + STRING_CONTENT, + TRUE, + WHITESPACE, + EOF +} + + +class Message internal constructor(val message: String, val severity: MessageSeverity, val start: Position, val end: Position) + + +enum class MessageSeverity { + ERROR, + WARNING, +} + + +class Position internal constructor(val line: Int, val column: Int) + + +enum class KsonValueType { + OBJECT, + ARRAY, + STRING, + INTEGER, + DECIMAL, + BOOLEAN, + NULL, + EMBED +} + +sealed class KsonValue(val start: Position, val end: Position) { + + abstract val type: KsonValueType + + + @ConsistentCopyVisibility + data class KsonObject internal constructor( + val properties: Map, + val propertyKeys: Map, + private val internalStart: Position, + private val internalEnd: Position + ) : KsonValue(internalStart, internalEnd) { + override val type = KsonValueType.OBJECT + } + + + class KsonArray internal constructor( + val elements: List, + internalStart: Position, + internalEnd: Position + ) : KsonValue(internalStart, internalEnd) { + override val type = KsonValueType.ARRAY + } + + + class KsonString internal constructor( + val value: String, + internalStart: Position, + internalEnd: Position + ) : KsonValue(internalStart, internalEnd) { + override val type = KsonValueType.STRING + } + + + sealed class KsonNumber(start: Position, end: Position) : KsonValue(start, end) { + + class Integer internal constructor( + val value: Int, + val internalStart: Position, + val internalEnd: Position + ) : KsonNumber(internalStart, internalEnd) { + override val type = KsonValueType.INTEGER + } + + class Decimal internal constructor( + val value: Double, + internalStart: Position, + internalEnd: Position + ) : KsonNumber(internalStart, internalEnd) { + override val type = KsonValueType.DECIMAL + } + } + + + class KsonBoolean internal constructor( + val value: Boolean, + internalStart: Position, + internalEnd: Position + ) : KsonValue(internalStart, internalEnd) { + override val type = KsonValueType.BOOLEAN + } + + + class KsonNull internal constructor( + internalStart: Position, + internalEnd: Position + ) : KsonValue(internalStart, internalEnd) { + override val type = KsonValueType.NULL + } + + + class KsonEmbed internal constructor( + val tag: String?, + val content: String, + internalStart: Position, + internalEnd: Position + ) : KsonValue(internalStart, internalEnd) { + override val type = KsonValueType.EMBED + } +} + +// --- JSON response parsing helpers --- + +internal fun parsePosition(obj: JsonObject): Position { + return Position( + line = obj["line"]!!.jsonPrimitive.int, + column = obj["column"]!!.jsonPrimitive.int + ) +} + +internal fun parseMessages(array: JsonArray): List { + return array.map { element -> + val obj = element.jsonObject + Message( + message = obj["message"]!!.jsonPrimitive.content, + severity = MessageSeverity.valueOf(obj["severity"]!!.jsonPrimitive.content), + start = parsePosition(obj["start"]!!.jsonObject), + end = parsePosition(obj["end"]!!.jsonObject) + ) + } +} + +internal fun parseTokens(array: JsonArray): List { + return array.map { element -> + val obj = element.jsonObject + Token( + tokenType = TokenType.valueOf(obj["tokenType"]!!.jsonPrimitive.content), + text = obj["text"]!!.jsonPrimitive.content, + start = parsePosition(obj["start"]!!.jsonObject), + end = parsePosition(obj["end"]!!.jsonObject) + ) + } +} + +internal fun parseKsonValue(obj: JsonObject): KsonValue { + val start = parsePosition(obj["start"]!!.jsonObject) + val end = parsePosition(obj["end"]!!.jsonObject) + + return when (val type = obj["type"]!!.jsonPrimitive.content) { + "OBJECT" -> { + val properties = obj["properties"]!!.jsonObject.mapValues { (_, v) -> parseKsonValue(v.jsonObject) } + val propertyKeys = obj["propertyKeys"]!!.jsonObject.mapValues { (_, v) -> + val keyObj = v.jsonObject + KsonValue.KsonString( + value = keyObj["value"]!!.jsonPrimitive.content, + internalStart = parsePosition(keyObj["start"]!!.jsonObject), + internalEnd = parsePosition(keyObj["end"]!!.jsonObject) + ) + } + KsonValue.KsonObject(properties, propertyKeys, start, end) + } + "ARRAY" -> { + val elements = obj["elements"]!!.jsonArray.map { parseKsonValue(it.jsonObject) } + KsonValue.KsonArray(elements, start, end) + } + "STRING" -> KsonValue.KsonString(obj["value"]!!.jsonPrimitive.content, start, end) + "INTEGER" -> KsonValue.KsonNumber.Integer(obj["value"]!!.jsonPrimitive.int, start, end) + "DECIMAL" -> KsonValue.KsonNumber.Decimal(obj["value"]!!.jsonPrimitive.double, start, end) + "BOOLEAN" -> KsonValue.KsonBoolean(obj["value"]!!.jsonPrimitive.boolean, start, end) + "NULL" -> KsonValue.KsonNull(start, end) + "EMBED" -> KsonValue.KsonEmbed( + tag = obj["tag"]?.let { if (it is JsonNull) null else it.jsonPrimitive.content }, + content = obj["content"]!!.jsonPrimitive.content, + internalStart = start, + internalEnd = end + ) + else -> throw IllegalArgumentException("Unknown KsonValue type: $type") + } +} diff --git a/kson-lib/build.gradle.kts b/kson-lib/build.gradle.kts index 26d76bcf..6db44e4b 100644 --- a/kson-lib/build.gradle.kts +++ b/kson-lib/build.gradle.kts @@ -10,7 +10,7 @@ plugins { kotlin("multiplatform") id("com.vanniktech.maven.publish") version "0.30.0" id("org.jetbrains.dokka") version "2.0.0" - id("nl.ochagavia.krossover") version "1.0.5" + id("nl.ochagavia.krossover") version "1.0.6" } repositories { diff --git a/kson-lib/src/commonMain/kotlin/org/kson/Kson.kt b/kson-lib/src/commonMain/kotlin/org/kson/Kson.kt index 58f20f13..00d0cfcc 100644 --- a/kson-lib/src/commonMain/kotlin/org/kson/Kson.kt +++ b/kson-lib/src/commonMain/kotlin/org/kson/Kson.kt @@ -216,7 +216,7 @@ class EmbedRule private constructor( return try { EmbedRuleResult.Success(EmbedRule(pathPattern, tag)) } catch (e: IllegalArgumentException) { - EmbedRuleResult.Failure(e.toString()) + EmbedRuleResult.Failure(e.message ?: "") } } } diff --git a/lib-python/kson-lib-tests/build.gradle.kts b/lib-python/kson-lib-tests/build.gradle.kts new file mode 100644 index 00000000..c4f82005 --- /dev/null +++ b/lib-python/kson-lib-tests/build.gradle.kts @@ -0,0 +1,54 @@ +plugins { + kotlin("multiplatform") +} + +repositories { + mavenCentral() +} + +val syncCommonTestSources by tasks.registering(Sync::class) { + from(project(":kson-lib").file("src/commonTest/kotlin")) + into(layout.buildDirectory.dir("commonTestSources")) +} + +val syncJvmTestSources by tasks.registering(Sync::class) { + from(project(":kson-lib").file("src/jvmTest/kotlin")) + into(layout.buildDirectory.dir("jvmTestSources")) +} + +tasks.withType { + dependsOn(":lib-python:build") + + systemProperty("libPythonDir", project(":lib-python").projectDir.absolutePath) + + useJUnitPlatform() + jvmArgs("-Djunit.jupiter.extensions.autodetection.enabled=true") +} + +kotlin { + jvm() + + sourceSets { + commonTest { + dependencies { + implementation(project(":kson-lib-http")) + implementation(kotlin("test")) + } + + kotlin { + srcDir(syncCommonTestSources) + } + } + jvmTest { + dependencies { + implementation("org.junit.jupiter:junit-jupiter-api:5.14.2") + runtimeOnly("org.junit.jupiter:junit-jupiter-engine:5.14.2") + } + + kotlin { + srcDir(syncJvmTestSources) + } + } + } +} + diff --git a/lib-python/kson-lib-tests/src/jvmTest/kotlin/ServerExtension.kt b/lib-python/kson-lib-tests/src/jvmTest/kotlin/ServerExtension.kt new file mode 100644 index 00000000..1ab56b14 --- /dev/null +++ b/lib-python/kson-lib-tests/src/jvmTest/kotlin/ServerExtension.kt @@ -0,0 +1,56 @@ +import org.junit.jupiter.api.extension.BeforeAllCallback +import org.junit.jupiter.api.extension.ExtensionContext +import org.kson.Kson +import java.io.File +import java.net.Socket + +/** + * A JUnit extension that makes sure the Python KSON server is running before all tests and gets + * shut down at the end + */ +class ServerExtension : BeforeAllCallback, AutoCloseable { + + companion object { + private var started = false + private var process: Process? = null + } + + override fun beforeAll(context: ExtensionContext) { + if (!started) { + started = true + + // Register for cleanup when the root context closes (after all tests) + context.root.getStore(ExtensionContext.Namespace.GLOBAL) + .put("serverExtension", this) + + val libPythonDir = File(System.getProperty("libPythonDir")) + val isWindows = System.getProperty("os.name").lowercase().contains("win") + val uvwCommand = if (isWindows) listOf("cmd", "/c", "uvw.bat") else listOf("./uvw") + val port = 8082 + + Kson.setPort(port) + + process = ProcessBuilder(uvwCommand + listOf("run", "python", "tests/api_server.py", port.toString())) + .directory(libPythonDir) + .redirectOutput(ProcessBuilder.Redirect.INHERIT) + .redirectErrorStream(true) + .start() + + // Wait for readiness + repeat(30) { + try { + Socket("localhost", port).close() + return + } catch (_: Exception) { + Thread.sleep(1000) + } + } + throw RuntimeException("Server did not start in time") + } + } + + override fun close() { + process?.destroy() + process?.waitFor() + } +} diff --git a/lib-python/kson-lib-tests/src/jvmTest/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/lib-python/kson-lib-tests/src/jvmTest/resources/META-INF/services/org.junit.jupiter.api.extension.Extension new file mode 100644 index 00000000..849fdf0a --- /dev/null +++ b/lib-python/kson-lib-tests/src/jvmTest/resources/META-INF/services/org.junit.jupiter.api.extension.Extension @@ -0,0 +1 @@ +ServerExtension diff --git a/lib-python/src/kson/__init__.py b/lib-python/src/kson/__init__.py index 0f46f3c2..a3c32d29 100644 --- a/lib-python/src/kson/__init__.py +++ b/lib-python/src/kson/__init__.py @@ -2017,9 +2017,9 @@ def from_path_pattern( if path_pattern is None: raise ValueError("`path_pattern` cannot be None") - jni_ref = _access_static_field(b"org/kson/EmbedRule", b"INSTANCE", b"Lorg/kson/EmbedRule;") + jni_ref = _access_static_field(b"org/kson/EmbedRule", b"Companion", b"Lorg/kson/EmbedRule$Companion;") result = _call_method( - b"org/kson/EmbedRule", + b"org/kson/EmbedRule$Companion", jni_ref, b"fromPathPattern", b"(Ljava/lang/String;Ljava/lang/String;)Lorg/kson/EmbedRuleResult;", @@ -2117,6 +2117,8 @@ class _IndentType_Tabs(IndentType): _jni_ref: Any + def __init__(self): + self._jni_ref = _access_static_field(b"org/kson/IndentType$Tabs", b"INSTANCE", b"Lorg/kson/IndentType$Tabs;") def __eq__(self, other): return _call_method(b"java/lang/Object", self._jni_ref, b"equals", b"(Ljava/lang/Object;)Z", "BooleanMethod", [other._jni_ref]) diff --git a/lib-python/tests/api_server.py b/lib-python/tests/api_server.py new file mode 100644 index 00000000..38914f23 --- /dev/null +++ b/lib-python/tests/api_server.py @@ -0,0 +1,280 @@ +"""HTTP server exposing the KSON Python API, conforming to kson-api-schema.json.""" + +import json +import sys +from http.server import HTTPServer, BaseHTTPRequestHandler +from kson import ( + Kson, FormatOptions, IndentType, FormattingStyle, EmbedRule, EmbedRuleResult, + TranspileOptions, Result, SchemaResult, KsonValue, KsonValueType, + MessageSeverity, +) + + +def _serialize_position(pos): + return {"line": pos.line(), "column": pos.column()} + + +def _serialize_message(msg): + return { + "message": msg.message(), + "severity": msg.severity().name, + "start": _serialize_position(msg.start()), + "end": _serialize_position(msg.end()), + } + + +def _serialize_token(token): + return { + "tokenType": token.token_type().name, + "text": token.text(), + "start": _serialize_position(token.start()), + "end": _serialize_position(token.end()), + } + + +def _serialize_kson_value(value): + if value is None: + return None + + vtype = value.type() + + if vtype == KsonValueType.OBJECT: + props = value.properties() + prop_keys = value.property_keys() + return { + "type": "OBJECT", + "properties": {k: _serialize_kson_value(v) for k, v in props.items()}, + "propertyKeys": {k: _serialize_kson_value(v) for k, v in prop_keys.items()}, + "start": _serialize_position(value.start()), + "end": _serialize_position(value.end()), + } + elif vtype == KsonValueType.ARRAY: + return { + "type": "ARRAY", + "elements": [_serialize_kson_value(e) for e in value.elements()], + "start": _serialize_position(value.start()), + "end": _serialize_position(value.end()), + } + elif vtype == KsonValueType.STRING: + return { + "type": "STRING", + "value": value.value(), + "start": _serialize_position(value.start()), + "end": _serialize_position(value.end()), + } + elif vtype == KsonValueType.INTEGER: + return { + "type": "INTEGER", + "value": value.value(), + "start": _serialize_position(value.start()), + "end": _serialize_position(value.end()), + } + elif vtype == KsonValueType.DECIMAL: + return { + "type": "DECIMAL", + "value": value.value(), + "start": _serialize_position(value.start()), + "end": _serialize_position(value.end()), + } + elif vtype == KsonValueType.BOOLEAN: + return { + "type": "BOOLEAN", + "value": bool(value.value()), + "start": _serialize_position(value.start()), + "end": _serialize_position(value.end()), + } + elif vtype == KsonValueType.NULL: + return { + "type": "NULL", + "start": _serialize_position(value.start()), + "end": _serialize_position(value.end()), + } + elif vtype == KsonValueType.EMBED: + return { + "type": "EMBED", + "tag": value.tag(), + "content": value.content(), + "start": _serialize_position(value.start()), + "end": _serialize_position(value.end()), + } + + +def _parse_indent_type(obj): + if obj is None: + return IndentType.Spaces(2) + if obj["type"] == "tabs": + return IndentType.Tabs() + return IndentType.Spaces(obj.get("size", 2)) + + +def _parse_formatting_style(name): + if name is None: + return FormattingStyle.PLAIN + return FormattingStyle[name] + + +def _parse_embed_rule(obj): + result = EmbedRule.from_path_pattern(obj["pathPattern"], obj.get("tag")) + assert isinstance(result, EmbedRuleResult.Success) + return result.embed_rule() + + +def _parse_format_options(obj): + if obj is None: + return FormatOptions(IndentType.Spaces(2), FormattingStyle.PLAIN, []) + indent = _parse_indent_type(obj.get("indentType")) + style = _parse_formatting_style(obj.get("formattingStyle")) + rules = [_parse_embed_rule(r) for r in obj.get("embedBlockRules", [])] + return FormatOptions(indent, style, rules) + + +def handle_format(req): + options = _parse_format_options(req.get("formatOptions")) + output = Kson.format(req["kson"], options) + return {"command": "format", "success": True, "output": output} + + +def handle_to_json(req): + retain = req.get("retainEmbedTags", True) + result = Kson.to_json(req["kson"], TranspileOptions.Json(retain_embed_tags=retain)) + if isinstance(result, Result.Success): + return {"command": "toJson", "success": True, "output": result.output()} + else: + return { + "command": "toJson", + "success": False, + "errors": [_serialize_message(m) for m in result.errors()], + } + + +def handle_to_yaml(req): + retain = req.get("retainEmbedTags", True) + result = Kson.to_yaml(req["kson"], TranspileOptions.Yaml(retain_embed_tags=retain)) + if isinstance(result, Result.Success): + return {"command": "toYaml", "success": True, "output": result.output()} + else: + return { + "command": "toYaml", + "success": False, + "errors": [_serialize_message(m) for m in result.errors()], + } + + +def handle_analyze(req): + filepath = req.get("filepath") + analysis = Kson.analyze(req["kson"], filepath) + return { + "command": "analyze", + "errors": [_serialize_message(m) for m in analysis.errors()], + "tokens": [_serialize_token(t) for t in analysis.tokens()], + "ksonValue": _serialize_kson_value(analysis.kson_value()), + } + + +def handle_parse_schema(req): + result = Kson.parse_schema(req["schemaKson"]) + if isinstance(result, SchemaResult.Success): + return {"command": "parseSchema", "success": True} + else: + return { + "command": "parseSchema", + "success": False, + "errors": [_serialize_message(m) for m in result.errors()], + } + + +def handle_validate_embed_rule(req): + rule_obj = req["embedBlockRule"] + result = EmbedRule.from_path_pattern(rule_obj["pathPattern"], rule_obj.get("tag")) + if isinstance(result, EmbedRuleResult.Success): + return {"command": "validateEmbedRule", "success": True} + else: + return {"command": "validateEmbedRule", "success": False, "error": result.message()} + + +def handle_validate(req): + schema_result = Kson.parse_schema(req["schemaKson"]) + if isinstance(schema_result, SchemaResult.Failure): + return { + "command": "validate", + "success": False, + "errors": [_serialize_message(m) for m in schema_result.errors()], + } + + validator = schema_result.schema_validator() + errors = validator.validate(req["kson"], req.get("filepath")) + return { + "command": "validate", + "success": len(errors) == 0, + "errors": [_serialize_message(m) for m in errors], + } + + +HANDLERS = { + "format": handle_format, + "toJson": handle_to_json, + "toYaml": handle_to_yaml, + "analyze": handle_analyze, + "parseSchema": handle_parse_schema, + "validate": handle_validate, + "validateEmbedRule": handle_validate_embed_rule, +} + + +class KsonHandler(BaseHTTPRequestHandler): + def do_POST(self): + length = int(self.headers.get("Content-Length", 0)) + body = self.rfile.read(length) + + try: + req = json.loads(body) + except json.JSONDecodeError as e: + self._send_error(400, f"Invalid JSON: {e}") + return + + command = req.get("command") + handler = HANDLERS.get(command) + if handler is None: + self._send_error(400, f"Unknown command: {command}") + return + + try: + response = handler(req) + except Exception as e: + self._send_error(500, str(e)) + return + + self._send_json(200, response) + + def _send_json(self, status, obj): + data = json.dumps(obj).encode("utf-8") + self.send_response(status) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + + def _send_error(self, status, message): + self._send_json(status, {"internal_error": message}) + + def log_message(self, format, *args): + # Suppress default stderr logging + pass + + +def main(): + port = int(sys.argv[1]) if len(sys.argv) > 1 else 8080 + server = HTTPServer(("127.0.0.1", port), KsonHandler) + print(f"Listening on http://127.0.0.1:{port}", flush=True) + try: + server.serve_forever() + except KeyboardInterrupt: + pass + except BaseException as e: + print(f"Exception! {e}") + print("Shutting down...") + server.server_close() + + +if __name__ == "__main__": + main() diff --git a/lib-rust/.gitignore b/lib-rust/.gitignore deleted file mode 100644 index 6cba219f..00000000 --- a/lib-rust/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -artifacts -kotlin diff --git a/lib-rust/kson-lib-tests/build.gradle.kts b/lib-rust/kson-lib-tests/build.gradle.kts new file mode 100644 index 00000000..4841c624 --- /dev/null +++ b/lib-rust/kson-lib-tests/build.gradle.kts @@ -0,0 +1,76 @@ +plugins { + kotlin("multiplatform") +} + +repositories { + mavenCentral() +} + +val releaseBuildDir: String = project(":lib-rust").projectDir.resolve("kson-test-server/target/release").absolutePath + +val syncCommonTestSources by tasks.registering(Sync::class) { + from(project(":kson-lib").file("src/commonTest/kotlin")) + into(layout.buildDirectory.dir("commonTestSources")) +} + +val syncJvmTestSources by tasks.registering(Sync::class) { + from(project(":kson-lib").file("src/jvmTest/kotlin")) + into(layout.buildDirectory.dir("jvmTestSources")) +} + +val buildTestServer by tasks.registering(Exec::class) { + dependsOn(":kson-lib:buildWithGraalVmNativeImage") + + val nativeArtifactsDir = project(":kson-lib").projectDir.resolve("build/kotlin/compileGraalVmNativeImage").absolutePath + + environment( + Pair("KSON_PREBUILT_BIN_DIR", nativeArtifactsDir), + Pair("KSON_COPY_SHARED_LIBRARY_TO_DIR", releaseBuildDir), + ) + + val cargoManifestPath = project(":lib-rust").projectDir.resolve("kson-test-server/Cargo.toml") + + group = "build" + workingDir = project(":lib-rust").projectDir + commandLine = "./pixiw run cargo build --release --manifest-path $cargoManifestPath".split(" ") + standardOutput = System.out + errorOutput = System.err + isIgnoreExitValue = false +} + +tasks.withType { + dependsOn(buildTestServer) + + systemProperty("releaseBuildDir", releaseBuildDir) + + useJUnitPlatform() + jvmArgs("-Djunit.jupiter.extensions.autodetection.enabled=true") +} + +kotlin { + jvm() + + sourceSets { + commonTest { + dependencies { + implementation(project(":kson-lib-http")) + implementation(kotlin("test")) + } + + kotlin { + srcDir(syncCommonTestSources) + } + } + jvmTest { + dependencies { + implementation("org.junit.jupiter:junit-jupiter-api:5.14.2") + runtimeOnly("org.junit.jupiter:junit-jupiter-engine:5.14.2") + } + + kotlin { + srcDir(syncJvmTestSources) + } + } + } +} + diff --git a/lib-rust/kson-lib-tests/src/jvmTest/kotlin/ServerExtension.kt b/lib-rust/kson-lib-tests/src/jvmTest/kotlin/ServerExtension.kt new file mode 100644 index 00000000..59133d62 --- /dev/null +++ b/lib-rust/kson-lib-tests/src/jvmTest/kotlin/ServerExtension.kt @@ -0,0 +1,56 @@ +import org.junit.jupiter.api.extension.BeforeAllCallback +import org.junit.jupiter.api.extension.ExtensionContext +import org.kson.Kson +import java.io.File +import java.net.Socket + +/** + * A JUnit extension that makes sure a KSON server is running before all tests and gets + * shut down at the end + */ +class ServerExtension : BeforeAllCallback, AutoCloseable { + + companion object { + private var started = false + private var process: Process? = null + } + + override fun beforeAll(context: ExtensionContext) { + if (!started) { + started = true + + // Register for cleanup when the root context closes (after all tests) + context.root.getStore(ExtensionContext.Namespace.GLOBAL) + .put("serverExtension", this) + + val releaseBuildDir = File(System.getProperty("releaseBuildDir")) + val port = 8081 + Kson.setPort(port) + + val processBuilder = ProcessBuilder(listOf("./kson-test-server", port.toString())) + .directory(releaseBuildDir) + .redirectOutput(ProcessBuilder.Redirect.INHERIT) + .redirectErrorStream(true); + + processBuilder.environment()["LD_LIBRARY_PATH"] = releaseBuildDir.absolutePath + + process = processBuilder.start() + + // Wait for readiness + repeat(30) { + try { + Socket("localhost", port).close() + return + } catch (_: Exception) { + Thread.sleep(1000) + } + } + throw RuntimeException("Server did not start in time") + } + } + + override fun close() { + process?.destroy() + process?.waitFor() + } +} diff --git a/lib-rust/kson-lib-tests/src/jvmTest/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/lib-rust/kson-lib-tests/src/jvmTest/resources/META-INF/services/org.junit.jupiter.api.extension.Extension new file mode 100644 index 00000000..849fdf0a --- /dev/null +++ b/lib-rust/kson-lib-tests/src/jvmTest/resources/META-INF/services/org.junit.jupiter.api.extension.Extension @@ -0,0 +1 @@ +ServerExtension diff --git a/lib-rust/kson-test-server/Cargo.lock b/lib-rust/kson-test-server/Cargo.lock new file mode 100644 index 00000000..dc4827f7 --- /dev/null +++ b/lib-rust/kson-test-server/Cargo.lock @@ -0,0 +1,1103 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bindgen" +version = "0.71.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "kson-rs" +version = "0.3.0-dev" +dependencies = [ + "kson-sys", +] + +[[package]] +name = "kson-sys" +version = "0.3.0-dev" +dependencies = [ + "anyhow", + "bindgen", + "flate2", + "tar", + "ureq", +] + +[[package]] +name = "kson-test-server" +version = "0.1.0" +dependencies = [ + "axum", + "kson-rs", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc97a28575b85cfedf2a7e7d3cc64b3e11bd8ac766666318003abbacc7a21fc" +dependencies = [ + "base64", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "ureq-proto", + "utf-8", + "webpki-roots", +] + +[[package]] +name = "ureq-proto" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f" +dependencies = [ + "base64", + "http", + "httparse", + "log", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/lib-rust/kson-test-server/Cargo.toml b/lib-rust/kson-test-server/Cargo.toml new file mode 100644 index 00000000..89db7acc --- /dev/null +++ b/lib-rust/kson-test-server/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "kson-test-server" +version = "0.1.0" +edition = "2024" + +[profile.release] +rpath = true + +[dependencies] +kson = { package = "kson-rs", path = "../kson" } +axum = "0.8" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } diff --git a/lib-rust/kson-test-server/src/main.rs b/lib-rust/kson-test-server/src/main.rs new file mode 100644 index 00000000..7b38fbf0 --- /dev/null +++ b/lib-rust/kson-test-server/src/main.rs @@ -0,0 +1,438 @@ +use axum::{Router, routing::post, Json}; +use serde::Deserialize; +use serde_json::Value; + +use kson::{ + EmbedRule, EmbedRuleResult, FormatOptions, FormattingStyle, IndentType, Kson, KsonValue, + indent_type, kson_value, transpile_options, +}; + +// --- Request types --- + +#[derive(Deserialize)] +#[serde(tag = "command")] +enum Request { + #[serde(rename = "format")] + Format { + kson: String, + #[serde(rename = "formatOptions")] + format_options: Option, + }, + #[serde(rename = "toJson")] + ToJson { + kson: String, + #[serde(rename = "retainEmbedTags", default = "default_true")] + retain_embed_tags: bool, + }, + #[serde(rename = "toYaml")] + ToYaml { + kson: String, + #[serde(rename = "retainEmbedTags", default = "default_true")] + retain_embed_tags: bool, + }, + #[serde(rename = "analyze")] + Analyze { + kson: String, + filepath: Option, + }, + #[serde(rename = "parseSchema")] + ParseSchema { + #[serde(rename = "schemaKson")] + schema_kson: String, + }, + #[serde(rename = "validate")] + Validate { + #[serde(rename = "schemaKson")] + schema_kson: String, + kson: String, + filepath: Option, + }, + #[serde(rename = "validateEmbedRule")] + ValidateEmbedRule { + #[serde(rename = "embedBlockRule")] + embed_block_rule: EmbedRuleDto, + }, +} + +fn default_true() -> bool { + true +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct FormatOptionsDto { + indent_type: Option, + formatting_style: Option, + embed_block_rules: Option>, +} + +#[derive(Deserialize)] +struct IndentTypeDto { + r#type: String, + size: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct EmbedRuleDto { + path_pattern: String, + tag: Option, +} + +// --- Response serialization helpers --- + +fn serialize_position(pos: kson::Position) -> Value { + serde_json::json!({ + "line": pos.line(), + "column": pos.column(), + }) +} + +fn serialize_message(msg: kson::Message) -> Value { + let severity = match msg.severity() { + kson::MessageSeverity::Error => "ERROR", + kson::MessageSeverity::Warning => "WARNING", + }; + serde_json::json!({ + "message": msg.message(), + "severity": severity, + "start": serialize_position(msg.start()), + "end": serialize_position(msg.end()), + }) +} + +fn serialize_token(token: kson::Token) -> Value { + let token_type = match token.token_type() { + kson::TokenType::CurlyBraceL => "CURLY_BRACE_L", + kson::TokenType::CurlyBraceR => "CURLY_BRACE_R", + kson::TokenType::SquareBracketL => "SQUARE_BRACKET_L", + kson::TokenType::SquareBracketR => "SQUARE_BRACKET_R", + kson::TokenType::AngleBracketL => "ANGLE_BRACKET_L", + kson::TokenType::AngleBracketR => "ANGLE_BRACKET_R", + kson::TokenType::Colon => "COLON", + kson::TokenType::Dot => "DOT", + kson::TokenType::EndDash => "END_DASH", + kson::TokenType::Comma => "COMMA", + kson::TokenType::Comment => "COMMENT", + kson::TokenType::EmbedOpenDelim => "EMBED_OPEN_DELIM", + kson::TokenType::EmbedCloseDelim => "EMBED_CLOSE_DELIM", + kson::TokenType::EmbedTag => "EMBED_TAG", + kson::TokenType::EmbedPreambleNewline => "EMBED_PREAMBLE_NEWLINE", + kson::TokenType::EmbedContent => "EMBED_CONTENT", + kson::TokenType::False => "FALSE", + kson::TokenType::UnquotedString => "UNQUOTED_STRING", + kson::TokenType::IllegalChar => "ILLEGAL_CHAR", + kson::TokenType::ListDash => "LIST_DASH", + kson::TokenType::Null => "NULL", + kson::TokenType::Number => "NUMBER", + kson::TokenType::StringOpenQuote => "STRING_OPEN_QUOTE", + kson::TokenType::StringCloseQuote => "STRING_CLOSE_QUOTE", + kson::TokenType::StringContent => "STRING_CONTENT", + kson::TokenType::True => "TRUE", + kson::TokenType::Whitespace => "WHITESPACE", + kson::TokenType::Eof => "EOF", + }; + serde_json::json!({ + "tokenType": token_type, + "text": token.text(), + "start": serialize_position(token.start()), + "end": serialize_position(token.end()), + }) +} + +fn serialize_kson_value(value: &KsonValue) -> Value { + match value { + KsonValue::KsonObject(obj) => { + let props = obj.properties(); + let property_keys = obj.property_keys(); + let mut serialized_props: serde_json::Map = serde_json::Map::new(); + for (key, val) in &props { + serialized_props.insert(key.clone(), serialize_kson_value(val)); + } + let mut serialized_keys: serde_json::Map = serde_json::Map::new(); + for (key, kson_str) in &property_keys { + serialized_keys.insert(key.clone(), serialize_kson_string(kson_str)); + } + serde_json::json!({ + "type": "OBJECT", + "properties": serialized_props, + "propertyKeys": serialized_keys, + "start": serialize_position(obj.start()), + "end": serialize_position(obj.end()), + }) + } + KsonValue::KsonArray(arr) => { + let elements: Vec = arr.elements().iter().map(serialize_kson_value).collect(); + serde_json::json!({ + "type": "ARRAY", + "elements": elements, + "start": serialize_position(arr.start()), + "end": serialize_position(arr.end()), + }) + } + KsonValue::KsonString(s) => serialize_kson_string(s), + KsonValue::KsonNumber(num) => match num { + kson_value::KsonNumber::Integer(i) => { + serde_json::json!({ + "type": "INTEGER", + "value": i.value(), + "start": serialize_position(i.start()), + "end": serialize_position(i.end()), + }) + } + kson_value::KsonNumber::Decimal(d) => { + serde_json::json!({ + "type": "DECIMAL", + "value": d.value(), + "start": serialize_position(d.start()), + "end": serialize_position(d.end()), + }) + } + }, + KsonValue::KsonBoolean(b) => { + serde_json::json!({ + "type": "BOOLEAN", + "value": b.value(), + "start": serialize_position(b.start()), + "end": serialize_position(b.end()), + }) + } + KsonValue::KsonNull(n) => { + serde_json::json!({ + "type": "NULL", + "start": serialize_position(n.start()), + "end": serialize_position(n.end()), + }) + } + KsonValue::KsonEmbed(e) => { + serde_json::json!({ + "type": "EMBED", + "tag": e.tag(), + "content": e.content(), + "start": serialize_position(e.start()), + "end": serialize_position(e.end()), + }) + } + } +} + +fn serialize_kson_string(s: &kson_value::KsonString) -> Value { + serde_json::json!({ + "type": "STRING", + "value": s.value(), + "start": serialize_position(s.start()), + "end": serialize_position(s.end()), + }) +} + +// --- Request handling --- + +fn convert_indent_type(dto: &IndentTypeDto) -> IndentType { + match dto.r#type.as_str() { + "tabs" => IndentType::Tabs(indent_type::Tabs::new()), + _ => IndentType::Spaces(indent_type::Spaces::new(dto.size.unwrap_or(2))), + } +} + +fn convert_formatting_style(s: &str) -> FormattingStyle { + match s { + "DELIMITED" => FormattingStyle::Delimited, + "COMPACT" => FormattingStyle::Compact, + "CLASSIC" => FormattingStyle::Classic, + _ => FormattingStyle::Plain, + } +} + +fn convert_format_options(dto: Option<&FormatOptionsDto>) -> FormatOptions { + let indent_type = dto + .and_then(|d| d.indent_type.as_ref()) + .map(convert_indent_type) + .unwrap_or_else(|| IndentType::Spaces(indent_type::Spaces::new(2))); + + let formatting_style = dto + .and_then(|d| d.formatting_style.as_deref()) + .map(convert_formatting_style) + .unwrap_or(FormattingStyle::Plain); + + let embed_rules: Vec = dto + .and_then(|d| d.embed_block_rules.as_ref()) + .map(|rules| { + rules + .iter() + .filter_map(|r| { + match EmbedRule::from_path_pattern( + &r.path_pattern, + r.tag.as_deref(), + ) { + EmbedRuleResult::Success(s) => Some(s.embed_rule()), + EmbedRuleResult::Failure(_) => None, + } + }) + .collect() + }) + .unwrap_or_default(); + + FormatOptions::new(indent_type, formatting_style, &embed_rules) +} + +fn handle_request(request: Request) -> Value { + match request { + Request::Format { + kson, + format_options, + } => { + let opts = convert_format_options(format_options.as_ref()); + let output = Kson::format(&kson, opts); + serde_json::json!({ + "command": "format", + "success": true, + "output": output, + }) + } + Request::ToJson { + kson, + retain_embed_tags, + } => { + let opts = transpile_options::Json::new(retain_embed_tags); + match Kson::to_json(&kson, opts) { + Ok(success) => serde_json::json!({ + "command": "toJson", + "success": true, + "output": success.output(), + }), + Err(failure) => { + let errors: Vec = + failure.errors().into_iter().map(serialize_message).collect(); + serde_json::json!({ + "command": "toJson", + "success": false, + "errors": errors, + }) + } + } + } + Request::ToYaml { + kson, + retain_embed_tags, + } => { + let opts = transpile_options::Yaml::new(retain_embed_tags); + match Kson::to_yaml(&kson, opts) { + Ok(success) => serde_json::json!({ + "command": "toYaml", + "success": true, + "output": success.output(), + }), + Err(failure) => { + let errors: Vec = + failure.errors().into_iter().map(serialize_message).collect(); + serde_json::json!({ + "command": "toYaml", + "success": false, + "errors": errors, + }) + } + } + } + Request::Analyze { kson, filepath } => { + let analysis = Kson::analyze(&kson, filepath.as_deref()); + let errors: Vec = analysis + .errors() + .into_iter() + .map(serialize_message) + .collect(); + let tokens: Vec = analysis + .tokens() + .into_iter() + .map(serialize_token) + .collect(); + let kson_value = analysis + .kson_value() + .as_ref() + .map(serialize_kson_value); + serde_json::json!({ + "command": "analyze", + "errors": errors, + "tokens": tokens, + "ksonValue": kson_value, + }) + } + Request::ParseSchema { schema_kson } => match Kson::parse_schema(&schema_kson) { + Ok(_) => serde_json::json!({ + "command": "parseSchema", + "success": true, + }), + Err(failure) => { + let errors: Vec = + failure.errors().into_iter().map(serialize_message).collect(); + serde_json::json!({ + "command": "parseSchema", + "success": false, + "errors": errors, + }) + } + }, + Request::Validate { + schema_kson, + kson, + filepath, + } => match Kson::parse_schema(&schema_kson) { + Ok(success) => { + let validator = success.schema_validator(); + let errors: Vec = validator + .validate(&kson, filepath.as_deref()) + .into_iter() + .map(serialize_message) + .collect(); + let is_success = errors.is_empty(); + serde_json::json!({ + "command": "validate", + "success": is_success, + "errors": errors, + }) + } + Err(failure) => { + let errors: Vec = + failure.errors().into_iter().map(serialize_message).collect(); + serde_json::json!({ + "command": "validate", + "success": false, + "errors": errors, + }) + } + }, + Request::ValidateEmbedRule { embed_block_rule } => { + match EmbedRule::from_path_pattern( + &embed_block_rule.path_pattern, + embed_block_rule.tag.as_deref(), + ) { + EmbedRuleResult::Success(_) => serde_json::json!({ + "command": "validateEmbedRule", + "success": true, + }), + EmbedRuleResult::Failure(failure) => serde_json::json!({ + "command": "validateEmbedRule", + "success": false, + "error": failure.message(), + }), + } + } + } +} + +async fn handler(Json(request): Json) -> Json { + Json(handle_request(request)) +} + +#[tokio::main] +async fn main() { + let port: u16 = std::env::args() + .nth(1) + .and_then(|s| s.parse().ok()) + .unwrap_or(3000); + + let app = Router::new().route("/", post(handler)); + let addr = std::net::SocketAddr::from(([127, 0, 0, 1], port)); + println!("Listening on {addr}"); + let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); + axum::serve(listener, app).await.unwrap(); +} diff --git a/lib-rust/kson/src/generated/mod.rs b/lib-rust/kson/src/generated/mod.rs index 50c1b7df..dabaec17 100644 --- a/lib-rust/kson/src/generated/mod.rs +++ b/lib-rust/kson/src/generated/mod.rs @@ -266,6 +266,62 @@ impl std::hash::Hash for Position { } + +#[derive(Clone)] +pub struct Companion { + kotlin_ptr: KotlinPtr, +} + +impl FromKotlinObject for Companion { + fn from_kotlin_object(obj: self::sys::jobject) -> Self { + let (env, _detach_guard) = util::attach_thread_to_java_vm(); + let kotlin_ptr = util::to_gc_global_ref(env, obj); + Self { kotlin_ptr } + } +} + +impl ToKotlinObject for Companion { + fn to_kotlin_object(&self) -> KotlinPtr { + self.kotlin_ptr.clone() + } +} + +impl AsKotlinObject for Companion { + fn as_kotlin_object(&self) -> self::sys::jobject { + self.kotlin_ptr.inner.inner + } +} + +impl Companion { +} + + +impl Companion { +} + +impl std::fmt::Debug for Companion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let obj = self.to_kotlin_object(); + write!(f, "{}", util::call_to_string(c"org/kson/EmbedRule$Companion", &obj)) + } +} + +impl Eq for Companion {} +impl PartialEq for Companion { + fn eq(&self, other: &Companion) -> bool { + util::equals(self.to_kotlin_object(), other.to_kotlin_object()) + } +} +impl std::hash::Hash for Companion { + fn hash(&self, state: &mut H) + where + H: std::hash::Hasher, + { + util::apply_hash_code(self.to_kotlin_object(), state) + } +} + + /// Represents a message logged during Kson processing #[derive(Clone)] pub struct Message { @@ -3581,7 +3637,7 @@ impl EmbedRule { path_pattern: &str, tag: Option<&str>, ) -> EmbedRuleResult { - let self_ptr = util::access_static_field(c"org/kson/EmbedRule", c"INSTANCE", c"Lorg/kson/EmbedRule;"); + let self_ptr = util::access_static_field(c"org/kson/EmbedRule", c"Companion", c"Lorg/kson/EmbedRule$Companion;"); let self_obj = self_ptr.as_kotlin_object(); let path_pattern_ptr = path_pattern.to_kotlin_object(); let path_pattern = path_pattern_ptr.as_kotlin_object(); @@ -3591,7 +3647,7 @@ impl EmbedRule { let (_, _detach_guard) = util::attach_thread_to_java_vm(); let result = call_jvm_function!( util, - c"org/kson/EmbedRule", + c"org/kson/EmbedRule$Companion", c"fromPathPattern", c"(Ljava/lang/String;Ljava/lang/String;)Lorg/kson/EmbedRuleResult;", CallObjectMethod, @@ -3758,6 +3814,10 @@ pub mod indent_type { } impl Tabs { + pub fn new() -> Self { + let kotlin_ptr = util::access_static_field(c"org/kson/IndentType$Tabs", c"INSTANCE", c"Lorg/kson/IndentType$Tabs;"); + Self { kotlin_ptr } + } } diff --git a/settings.gradle.kts b/settings.gradle.kts index ed22c46e..65138bf9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -14,9 +14,12 @@ pluginManagement { rootProject.name = "kson" include("native-tests") include("kson-lib") +include("kson-lib-http") include("kson-tooling-lib") include("lib-python") +include("lib-python:kson-lib-tests") include("lib-rust") +include("lib-rust:kson-lib-tests") include("tooling:jetbrains") include("tooling:language-server-protocol") include("tooling:lsp-clients")