Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -229,10 +229,8 @@ private fun generateSchemaSuiteTestClasses(
): String {
return """package $testClassPackage

import org.kson.CoreCompileConfig
import org.kson.Kson
import org.kson.schema.JsonSchemaTest
import kotlin.test.Test
import kotlin.test.assertEquals

/**
* DO NOT MANUALLY EDIT. This class is GENERATED by `./gradlew generateJsonTestSuite` task
Expand All @@ -242,12 +240,12 @@ import kotlin.test.assertEquals
* removing exclusions from [org.kson.jsonsuite.schemaTestSuiteExclusions]
*/
@Suppress("UNREACHABLE_CODE") // unreachable code is okay here until we complete the above TODO
class SchemaSuiteTest {
class SchemaSuiteTest : JsonSchemaTest {

${ tests.joinToString("\n") {
val theTests = ArrayList<String>()
for (schema in it.schemaTestGroups) {
val schemaComment = if (schema.comment != null) "// " + schema.comment + "\n" else ""
val schemaComment = if (schema.comment != null) "// " + schema.comment else ""
for (test in schema.tests) {
// construct a legal and unique name for this test
val schemaTestName = "${formatForTestName(it.testFileName)}_${formatForTestName(schema.description)}_${formatForTestName(test.description)}"
Expand All @@ -271,12 +269,13 @@ ${ tests.joinToString("\n") {
else {
""
}}
| $schemaComment
| assertKsonEnforcesSchema(
| ${"\"\"\""}
| ${formatForTest(test.data)}
| ${"\"\"\""},
| ${"\"\"\""}
| ${schemaComment}${formatForTest(schema.schema)}
| ${formatForTest(schema.schema)}
| ${"\"\"\""},
| ${test.valid},
| ${"\"\"\""}${formatForTest(schema.description)} -> ${formatForTest(test.description)}${"\"\"\""})
Expand All @@ -287,22 +286,6 @@ ${ tests.joinToString("\n") {
}
theTests.joinToString("\n\n")
}}

private fun assertKsonEnforcesSchema(ksonSource: String,
schemaJson: String,
shouldAcceptAsValid: Boolean,
description: String) {
// accepted as valid if and only if we parsed without error
val acceptedAsValid = !Kson.parseToAst(
ksonSource.trimIndent(),
coreCompileConfig = CoreCompileConfig(schemaJson = schemaJson.trimIndent()))
.hasErrors()

assertEquals(
shouldAcceptAsValid,
acceptedAsValid,
description)
}
}
"""
}
Expand Down

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion src/commonMain/kotlin/org/kson/Kson.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import org.kson.ast.*
import org.kson.stdlibx.collections.ImmutableList
import org.kson.parser.*
import org.kson.parser.messages.MessageType
import org.kson.schema.SchemaParser
import org.kson.tools.KsonFormatterConfig

/**
Expand Down Expand Up @@ -40,7 +41,13 @@ class Kson {
if (coreCompileConfig.schemaJson == NO_SCHEMA) {
return AstParseResult(ast, tokens, messageSink)
} else {
TODO("Json Schema support for Kson not yet implemented")
val schemaParseResult = SchemaParser.parse(coreCompileConfig.schemaJson)
val jsonSchema = schemaParseResult.jsonSchema
?: // schema todo make a schema parser entry point and suggest they run this through it to troubleshoot
throw IllegalStateException("Schema parse failed:\n" + LoggedMessage.print(schemaParseResult.messages))
// validate against our schema, logging any errors to our message sink
jsonSchema.validate(ast?.toKsonApi() as KsonValue, messageSink)
return AstParseResult(ast, tokens, messageSink)
}
}

Expand Down
125 changes: 117 additions & 8 deletions src/commonMain/kotlin/org/kson/ast/KsonApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,134 @@ import org.kson.parser.NumberParser
*/
sealed class KsonApi(val location: Location)

abstract class KsonValue(location: Location) : KsonApi(location)
abstract class KsonValue(location: Location) : KsonApi(location) {
/**
* Ensure all our [KsonValue] classes implement their [equals] and [hashCode]
*/
abstract override fun equals(other: Any?): Boolean
abstract override fun hashCode(): Int
}

class KsonObject(private val propertyList: List<KsonObjectProperty>, location: Location) : KsonValue(location) {
class KsonObject(val propertyList: List<KsonObjectProperty>, location: Location) : KsonValue(location) {
val propertyMap: Map<String, KsonValue> by lazy {
propertyList.associate { it.name.value to it.ksonValue }
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is KsonObject) return false

if (propertyMap.size != other.propertyMap.size) return false

return propertyMap.all { (key, value) ->
other.propertyMap[key]?.let { value == it } ?: false
}
}

override fun hashCode(): Int {
return propertyMap.hashCode()
}
}

class KsonList(val elements: List<KsonListElement>, location: Location) : KsonValue(location) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is KsonList) return false

if (elements.size != other.elements.size) return false

return elements.zip(other.elements).all { (a, b) ->
a.ksonValue == b.ksonValue
}
}

override fun hashCode(): Int {
return elements.map { it.ksonValue.hashCode() }.hashCode()
}
}
class KsonList(val elements: List<KsonListElement>, location: Location) : KsonValue(location)

class KsonListElement(val ksonValue: KsonValue, location: Location) : KsonApi(location)
class KsonObjectProperty(val name: KsonString,
val ksonValue: KsonValue,
location: Location) :KsonApi(location)
class EmbedBlock(val embedTag: String,
val embedContent: String,
location: Location) : KsonValue(location)
class KsonString(val value: String, location: Location) : KsonValue(location)
class KsonNumber(val value: NumberParser.ParsedNumber, location: Location) : KsonValue(location)
class KsonBoolean(val value: Boolean, location: Location) : KsonValue(location)
class KsonNull(location: Location) : KsonValue(location)
location: Location) : KsonValue(location) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is EmbedBlock) return false

return embedTag == other.embedTag && embedContent == other.embedContent
}

override fun hashCode(): Int {
return 31 * embedTag.hashCode() + embedContent.hashCode()
}
}

class KsonString(val value: String, location: Location) : KsonValue(location) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is KsonString) return false

return value == other.value
}

override fun hashCode(): Int {
return value.hashCode()
}
}

class KsonNumber(val value: NumberParser.ParsedNumber, location: Location) : KsonValue(location) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is KsonNumber) return false

// Numbers are equal if their numeric values are equal (supporting cross-type comparison)
val thisValue = when (value) {
is NumberParser.ParsedNumber.Integer -> value.value.toDouble()
is NumberParser.ParsedNumber.Decimal -> value.value
}
val otherValue = when (other.value) {
is NumberParser.ParsedNumber.Integer -> other.value.value.toDouble()
is NumberParser.ParsedNumber.Decimal -> other.value.value
}

return thisValue == otherValue
}

override fun hashCode(): Int {
// Use the double value for consistent hashing across integer/decimal representations
val doubleValue = when (value) {
is NumberParser.ParsedNumber.Integer -> value.value.toDouble()
is NumberParser.ParsedNumber.Decimal -> value.value
}
return doubleValue.hashCode()
}
}

class KsonBoolean(val value: Boolean, location: Location) : KsonValue(location) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is KsonBoolean) return false

return value == other.value
}

override fun hashCode(): Int {
return value.hashCode()
}
}

class KsonNull(location: Location) : KsonValue(location) {
override fun equals(other: Any?): Boolean {
return other is KsonNull
}

override fun hashCode(): Int {
return KsonNull::class.hashCode()
}
}

fun AstNode.toKsonApi(): KsonApi {
if (this !is AstNodeImpl) {
Expand Down
18 changes: 14 additions & 4 deletions src/commonMain/kotlin/org/kson/parser/NumberParser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,20 @@ class NumberParser(private val numberCandidate: String) {
private var hasDecimalPoint = false
private var hasExponent = false

sealed interface ParsedNumber {
val asString: String
sealed class ParsedNumber {
abstract val asString: String

class Integer(rawString: String) : ParsedNumber {
/**
* Get this [ParsedNumber] as a [Double]
*/
val asDouble: Double by lazy {
when (this) {
is Decimal -> value
is Integer -> value.toDouble()
}
}

class Integer(rawString: String) : ParsedNumber() {
override val asString = trimLeadingZeros(rawString)
val value = convertToLong(rawString.trimStart('0').ifEmpty { "0" })

Expand All @@ -49,7 +59,7 @@ class NumberParser(private val numberCandidate: String) {
}
}

class Decimal(rawString: String) : ParsedNumber {
class Decimal(rawString: String) : ParsedNumber() {
override val asString = trimLeadingZeros(rawString)
val value: Double by lazy {
asString.toDouble()
Expand Down
Loading