diff --git a/dataforge-meta/build.gradle.kts b/dataforge-meta/build.gradle.kts index 49d8bdc5..a4c67c9c 100644 --- a/dataforge-meta/build.gradle.kts +++ b/dataforge-meta/build.gradle.kts @@ -10,6 +10,12 @@ kscience { useSerialization { json() } + + commonTest { + dependencies { + implementation("io.github.optimumcode:json-schema-validator:0.5.2") + } + } } description = "Meta definition and basic operations on meta" diff --git a/dataforge-meta/src/commonMain/kotlin/space/kscience/dataforge/meta/descriptors/JsonSchema.kt b/dataforge-meta/src/commonMain/kotlin/space/kscience/dataforge/meta/descriptors/JsonSchema.kt new file mode 100644 index 00000000..39d9e10f --- /dev/null +++ b/dataforge-meta/src/commonMain/kotlin/space/kscience/dataforge/meta/descriptors/JsonSchema.kt @@ -0,0 +1,400 @@ +package space.kscience.dataforge.meta.descriptors + +/** + * A comprehensive JSON Schema implementation supporting Draft 2020-12 specification. + * + * This object provides: + * - Core JSON Schema vocabulary constants (like `$schema`, `$id`, `$ref`) + * - Standard validation keywords (type, enum, pattern, etc.) + * - Schema composition keywords (allOf, anyOf, oneOf) + * - Metadata annotations (title, description, examples) + * - Custom extensions for specialized use cases + * - Version management for JSON Schema specifications + * + * The structure follows the official JSON Schema organization with nested vocabulary objects: + * - [Vocabularies.Core]: Fundamental schema identification and referencing + * - [Vocabularies.Applicator]: Schema composition and combination + * - [Vocabularies.Validation]: Instance validation constraints + * - [Vocabularies.MetaData]: Descriptive metadata + * - [Vocabularies.Custom]: Domain-specific extensions + * + * @property VERSION The supported JSON Schema version (DRAFT_2020_12 by default) + * + * @see JSON Schema Core Specification + */ +public object JsonSchema { + public val VERSION: JsonSchemaVersion = JsonSchemaVersion.DRAFT_2020_12 + + /** + * A curated set of vocabularies defined for this JSON Schema dialect + */ + public object Vocabularies { + /** + * A mandatory vocabulary that defines keywords that are either required in order to process any schema + * or meta-schema, including those split across multiple documents, or exist to reserve keywords + * for purposes that require guaranteed interoperability + */ + public object Core { + /** + * This keyword declares an identifier for the schema resource + */ + public const val ID: String = "\$id" + + /** + * This keyword is both used as a JSON Schema dialect identifier and as a reference to a JSON Schema + * which describes the set of valid schemas written for this particular dialect + */ + public const val SCHEMA: String = "\$schema" + + /** + * This keyword is used to reference a statically identified schema + */ + public const val REF: String = "\$ref" + + /** + * This keyword reserves a location for comments from schema authors to readers or maintainers of the schema + */ + public const val COMMENT: String = "\$comment" + + /** + * This keyword reserves a location for schema authors to inline re-usable JSON Schemas into a more general schema + */ + public const val DEFS: String = "\$defs" + + /** + * This keyword is used to create plain name fragments that are not tied to any particular structural location + * for referencing purposes, which are taken into consideration for static referencing + */ + public const val ANCHOR: String = "\$anchor" + + /** + * This keyword is used to create plain name fragments that are not tied to any particular structural location + * for referencing purposes, which are taken into consideration for dynamic referencing + */ + public const val DYNAMIC_ANCHOR: String = "\$dynamicAnchor" + + /** + * This keyword is used to reference an identified schema, deferring the full resolution until runtime, + * at which point it is resolved each time it is encountered while evaluating an instance + */ + public const val DYNAMIC_REF: String = "\$dynamicRef" + + /** + * This keyword is used in dialect meta-schemas to identify the required and optional vocabularies + * available for use in schemas described by that dialect + */ + public const val VOCABULARY: String = "\$vocabulary" + } + + /** + * A vocabulary that defines applicator keywords that are recommended for use as the basis of other vocabularies + */ + public object Applicator { + /** + * An instance validates successfully against this keyword if it validates successfully against all schemas + * defined by this keyword's value + */ + public const val ALL_OF: String = "allOf" + + /** + * An instance validates successfully against this keyword if it validates successfully against at least one + * schema defined by this keyword's value + */ + public const val ANY_OF: String = "anyOf" + + /** + * An instance validates successfully against this keyword if it validates successfully against exactly one + * schema defined by this keyword's value + */ + public const val ONE_OF: String = "oneOf" + + /** + * This keyword declares a condition based on the validation result of the given schema + */ + public const val IF: String = "if" + + /** + * When if is present, and the instance successfully validates against its subschema, then validation succeeds + * if the instance also successfully validates against this keyword's subschema + */ + public const val THEN: String = "then" + + /** + * When if is present, and the instance fails to validate against its subschema, then validation succeeds + * if the instance successfully validates against this keyword's subschema + */ + public const val ELSE: String = "else" + + /** + * An instance is valid against this keyword if it fails to validate successfully against the schema + * defined by this keyword + */ + public const val NOT: String = "not" + + /** + * Validation succeeds if, for each name that appears in both the instance and as a name within this keyword's + * value, the child instance for that name successfully validates against the corresponding schema + */ + public const val PROPERTIES: String = "properties" + + /** + * Validation succeeds if the schema validates against each value not matched by other object applicators + * in this vocabulary + */ + public const val ADDITIONAL_PROPERTIES: String = "additionalProperties" + + /** + * Validation succeeds if, for each instance name that matches any regular expressions that appear as a + * property name in this keyword's value, the child instance for that name successfully validates against + * each schema that corresponds to a matching regular expression + */ + public const val PATTERN_PROPERTIES: String = "patternProperties" + + /** + * This keyword specifies subschemas that are evaluated if the instance is an object and contains a certain property + */ + public const val DEPENDENT_SCHEMAS: String = "dependentSchemas" + + /** + * Validation succeeds if the schema validates against every property name in the instance + */ + public const val PROPERTY_NAMES: String = "propertyNames" + + /** + * Validation succeeds if the instance contains an element that validates against this schema + */ + public const val CONTAINS: String = "contains" + + /** + * Validation succeeds if each element of the instance not covered by prefixItems validates against this schema + */ + public const val ITEMS: String = "items" + + /** + * Validation succeeds if each element of the instance validates against the schema at the same position, if any + */ + public const val PREFIX_ITEMS: String = "prefixItems" + } + + /** + * A vocabulary that defines keywords that impose requirements for successful validation of an instance + */ + public object Validation { + /** + * Validation succeeds if the type of the instance matches the type represented by the given type, + * or matches at least one of the given types + */ + public const val TYPE: String = "type" + + /** + * Validation succeeds if the instance is equal to one of the elements in this keyword's array value + */ + public const val ENUM: String = "enum" + + /** + * Validation succeeds if the instance is equal to this keyword's value + */ + public const val CONST: String = "const" + + /** + * A string instance is valid against this keyword if its length is less than, or equal to, the value of this keyword + */ + public const val MAX_LENGTH: String = "maxLength" + + /** + * A string instance is valid against this keyword if its length is greater than, or equal to, the value of this keyword + */ + public const val MIN_LENGTH: String = "minLength" + + /** + * A string instance is considered valid if the regular expression matches the instance successfully + */ + public const val PATTERN: String = "pattern" + + /** + * Validation succeeds if the numeric instance is less than the given number + */ + public const val EXCLUSIVE_MAXIMUM: String = "exclusiveMaximum" + + /** + * Validation succeeds if the numeric instance is greater than the given number + */ + public const val EXCLUSIVE_MINIMUM: String = "exclusiveMinimum" + + /** + * Validation succeeds if the numeric instance is less than or equal to the given number + */ + public const val MAXIMUM: String = "maximum" + + /** + * Validation succeeds if the numeric instance is greater than or equal to the given number + */ + public const val MINIMUM: String = "minimum" + + /** + * A numeric instance is valid only if division by this keyword's value results in an integer + */ + public const val MULTIPLE_OF: String = "multipleOf" + + /** + * Validation succeeds if, for each name that appears in both the instance and as a name within this keyword's + * value, every item in the corresponding array is also the name of a property in the instance + */ + public const val DEPENDENT_REQUIRED: String = "dependentRequired" + + /** + * An object instance is valid if its number of properties is less than, or equal to, the value of this keyword + */ + public const val MAX_PROPERTIES: String = "maxProperties" + + /** + * An object instance is valid if its number of properties is greater than, or equal to, the value of this keyword + */ + public const val MIN_PROPERTIES: String = "minProperties" + + /** + * An object instance is valid against this keyword if every item in the array is the name of a property in the instance + */ + public const val REQUIRED: String = "required" + + /** + * An array instance is valid if its size is less than, or equal to, the value of this keyword + */ + public const val MAX_ITEMS: String = "maxItems" + + /** + * An array instance is valid if its size is greater than, or equal to, the value of this keyword + */ + public const val MIN_ITEMS: String = "minItems" + + /** + * The number of times that the contains keyword (if set) successfully validates against the instance must be + * less than or equal to the given integer + */ + public const val MAX_CONTAINS: String = "maxContains" + + /** + * The number of times that the contains keyword (if set) successfully validates against the instance must be + * greater than or equal to the given integer + */ + public const val MIN_CONTAINS: String = "minContains" + + /** + * If this keyword is set to the boolean value true, the instance validates successfully if all of its elements are unique + */ + public const val UNIQUE_ITEMS: String = "uniqueItems" + } + + /** + * A vocabulary to defines general-purpose annotation keywords + */ + public object MetaData { + /** + * A preferably short description about the purpose of the instance described by the schema + */ + public const val TITLE: String = "title" + + /** + * An explanation about the purpose of the instance described by the schema + */ + public const val DESCRIPTION: String = "description" + + /** + * This keyword can be used to supply a default JSON value associated with a particular schema + */ + public const val DEFAULT: String = "default" + + /** + * This keyword indicates that applications should refrain from using the declared property + */ + public const val DEPRECATED: String = "deprecated" + + /** + * This keyword is used to provide sample JSON values associated with a particular schema + */ + public const val EXAMPLES: String = "examples" + + /** + * This keyword indicates that the value of the instance is managed exclusively by the owning authority + */ + public const val READ_ONLY: String = "readOnly" + + /** + * This keyword indicates that the value is never present when the instance is retrieved + */ + public const val WRITE_ONLY: String = "writeOnly" + } + + /** + * A vocabulary to defines semantic information about string-encoded values + */ + public object FormatAnnotation { + /** + * Define semantic information about a string instance + */ + public const val FORMAT: String = "format" + } + + /** + * A vocabulary for annotating instances that contain non-JSON data encoded in JSON strings + */ + public object Content { + /** + * The string instance should be interpreted as encoded binary data + */ + public const val CONTENT_ENCODING: String = "contentEncoding" + + /** + * This keyword declares the media type of the string instance + */ + public const val CONTENT_MEDIA_TYPE: String = "contentMediaType" + + /** + * This keyword declares a schema which describes the structure of the string + */ + public const val CONTENT_SCHEMA: String = "contentSchema" + } + + /** + * A vocabulary for applying subschemas to unevaluated array items or object properties + */ + public object Unevaluated { + /** + * Validates array elements that did not successfully validate against other standard array applicators + */ + public const val UNEVALUATED_ITEMS: String = "unevaluatedItems" + + /** + * Validates object properties that did not successfully validate against other standard object applicators + */ + public const val UNEVALUATED_PROPERTIES: String = "unevaluatedProperties" + } + + public object Custom { + /** + * An index field by which this node is identified in case of same name siblings construct + */ + public const val INDEX_KEY: String = "__indexKey__" + + /** + * Additional attributes of this descriptor. For example, validation and widget parameters + */ + public const val ATTRIBUTES: String = "__attributes__" + + /** + * True if same name siblings with this name are allowed + */ + public const val MULTIPLE: String = "__multiple__" + } + } + + public enum class JsonSchemaVersion(public val value: String) { + /** + * JSON Schema 2020-12 is a JSON media type for defining the structure of JSON data. + * JSON Schema is intended to define validation, documentation, hyperlink navigation, + * and interaction control of JSON data. + * @see Specification + */ + DRAFT_2020_12("https://json-schema.org/draft/2020-12/schema") + } +} \ No newline at end of file diff --git a/dataforge-meta/src/commonMain/kotlin/space/kscience/dataforge/meta/descriptors/MetaDescriptor.kt b/dataforge-meta/src/commonMain/kotlin/space/kscience/dataforge/meta/descriptors/MetaDescriptor.kt index 45954985..7930b579 100644 --- a/dataforge-meta/src/commonMain/kotlin/space/kscience/dataforge/meta/descriptors/MetaDescriptor.kt +++ b/dataforge-meta/src/commonMain/kotlin/space/kscience/dataforge/meta/descriptors/MetaDescriptor.kt @@ -71,7 +71,14 @@ public data class MetaDescriptor( } } -public val MetaDescriptor.required: Boolean get() = valueRestriction == ValueRestriction.REQUIRED || nodes.values.any { required } +public val MetaDescriptor.required: Boolean get() = checkRequired(hashSetOf()) + +private fun MetaDescriptor.checkRequired(visited: MutableSet): Boolean { + if (this in visited) return false + visited.add(this) + return valueRestriction == ValueRestriction.REQUIRED || + nodes.values.any { it.checkRequired(visited) } +} public val MetaDescriptor.allowedValues: List? get() = attributes[MetaDescriptor.ALLOWED_VALUES_KEY]?.value?.list diff --git a/dataforge-meta/src/commonMain/kotlin/space/kscience/dataforge/meta/descriptors/MetaDescriptorJsonSchema.kt b/dataforge-meta/src/commonMain/kotlin/space/kscience/dataforge/meta/descriptors/MetaDescriptorJsonSchema.kt new file mode 100644 index 00000000..224a903c --- /dev/null +++ b/dataforge-meta/src/commonMain/kotlin/space/kscience/dataforge/meta/descriptors/MetaDescriptorJsonSchema.kt @@ -0,0 +1,223 @@ +package space.kscience.dataforge.meta.descriptors + +import kotlinx.serialization.json.* +import space.kscience.dataforge.meta.* + +/** + * A converter between [MetaDescriptor] and JSON Schema ([JsonObject]) representations. + * + * Provides bidirectional conversion between metadata descriptors and JSON Schema format, + * handling: + * - Basic schema metadata (title, description) + * - Value type restrictions and validation + * - Allowed values (enums) + * - Required fields + * - Nested properties + * - Default values + * - Custom metadata fields (indexKey, multiple, attributes) + * + * The converter maintains JSON Schema compatibility while preserving all [MetaDescriptor] features. + */ +private object MetaDescriptorJsonSchemaConverter { + fun convertMetaDescriptorToJsonSchema(metaDescriptor: MetaDescriptor): JsonObject { + return convertMetaDescriptorToJsonSchema(metaDescriptor, 1, null) + } + + private fun convertMetaDescriptorToJsonSchema(metaDescriptor: MetaDescriptor, depth: Int, title: String?): JsonObject = buildJsonObject { + // Basic metadata + if (depth == 1) { + put(JsonSchema.Vocabularies.Core.SCHEMA, JsonSchema.VERSION.value) + } + + title?.let { put(JsonSchema.Vocabularies.MetaData.TITLE, it) } + metaDescriptor.description?.let { put(JsonSchema.Vocabularies.MetaData.DESCRIPTION, it) } + + // Value type handling + when (metaDescriptor.valueRestriction) { + ValueRestriction.ABSENT -> put(JsonSchema.Vocabularies.Validation.TYPE, JsonPrimitive("null")) + else -> { + metaDescriptor.valueTypes?.let { types -> + put( + JsonSchema.Vocabularies.Validation.TYPE, + if (types.size == 1) JsonPrimitive(valueTypeToJsonType(types[0])) + else buildJsonArray { types.mapToJsonTypes().forEach(::add) } + ) + } + } + } + + // Handle allowed values + metaDescriptor.allowedValues?.let { allowed -> + put(JsonSchema.Vocabularies.Validation.ENUM, buildJsonArray { + allowed.map { it.mapToJsonElement() }.forEach(::add) + }) + } + + // Handle required fields + val listOfRequired = metaDescriptor.nodes.filter { (_, node) -> node.required }.map { (name, _) -> name } + if (!listOfRequired.isEmpty()) { + put(JsonSchema.Vocabularies.Validation.REQUIRED, buildJsonArray { listOfRequired.forEach(::add) }) + } + + // Handle child nodes + if (metaDescriptor.nodes.isNotEmpty()) { + put( + JsonSchema.Vocabularies.Applicator.PROPERTIES, buildJsonObject { + metaDescriptor.nodes.forEach { (title, node) -> + put(title, convertMetaDescriptorToJsonSchema(node, depth + 1, title)) + } + }) + } + + // Handle default value + if (metaDescriptor.defaultValue != null) { + put(JsonSchema.Vocabularies.MetaData.DEFAULT, metaDescriptor.defaultValue.mapToJsonElement()) + } + + // Custom meta descriptor fields + put(JsonSchema.Vocabularies.Custom.INDEX_KEY, metaDescriptor.indexKey) + put(JsonSchema.Vocabularies.Custom.MULTIPLE, metaDescriptor.multiple) + put(JsonSchema.Vocabularies.Custom.ATTRIBUTES, Json.encodeToJsonElement(metaDescriptor.attributes)) + } + + fun convertJsonSchemaToMetaDescriptor(jsonObject: JsonObject): MetaDescriptor { + return convertJsonSchemaToMetaDescriptor(jsonObject, 1) + } + + private fun convertJsonSchemaToMetaDescriptor(jsonObject: JsonObject, depth: Int): MetaDescriptor { + val builder = MetaDescriptorBuilder() + + // Handle basic metadata + builder.description = jsonObject[JsonSchema.Vocabularies.MetaData.DESCRIPTION]?.jsonPrimitive?.contentOrNull + + // Handle value types + val typeElement = jsonObject[JsonSchema.Vocabularies.Validation.TYPE] + val valueTypes = when (typeElement) { + is JsonPrimitive -> listOfNotNull(jsonTypeToValueType(typeElement.contentOrNull)) + is JsonArray -> typeElement.mapNotNull { jsonTypeToValueType(it.jsonPrimitive.contentOrNull) } + else -> null + } + builder.valueTypes = valueTypes?.takeIf { it.isNotEmpty() } + + // Handle value restriction + builder.valueRestriction = when { + valueTypes?.contains(ValueType.NULL) == true && valueTypes.size == 1 -> ValueRestriction.ABSENT + jsonObject[JsonSchema.Vocabularies.Validation.REQUIRED] != null -> ValueRestriction.REQUIRED + else -> ValueRestriction.NONE + } + + // Handle allowed values + jsonObject[JsonSchema.Vocabularies.Validation.ENUM]?.jsonArray?.let { enumArray -> + builder.allowedValues = enumArray.map { jsonElement -> + when (jsonElement) { + is JsonPrimitive -> when { + jsonElement.isString -> StringValue(jsonElement.content) + jsonElement.booleanOrNull != null -> if (jsonElement.boolean) True else False + jsonElement.doubleOrNull != null -> NumberValue(jsonElement.double) + else -> Null + } + is JsonArray -> ListValue(jsonElement.map { it.mapToValue() }) + else -> Null + } + } + } + + // Handle default value + jsonObject[JsonSchema.Vocabularies.MetaData.DEFAULT]?.let { defaultValue -> + builder.default = defaultValue.mapToValue() + } + + // Handle child nodes + jsonObject[JsonSchema.Vocabularies.Applicator.PROPERTIES]?.jsonObject?.let { properties -> + properties.forEach { (name, schema) -> + builder.node(name, convertJsonSchemaToMetaDescriptor(schema.jsonObject, depth + 1)) + } + } + + // Handle required fields + val requiredFields = jsonObject[JsonSchema.Vocabularies.Validation.REQUIRED]?.jsonArray?.mapNotNull { it.jsonPrimitive.contentOrNull } ?: emptyList() + if (requiredFields.isNotEmpty()) { + builder.children.forEach { (name, childBuilder) -> + if (name in requiredFields) { + childBuilder.valueRestriction = ValueRestriction.REQUIRED + } + } + } + + // Handle custom fields + builder.indexKey = jsonObject[JsonSchema.Vocabularies.Custom.INDEX_KEY]?.jsonPrimitive?.contentOrNull ?: Meta.INDEX_KEY + builder.multiple = jsonObject[JsonSchema.Vocabularies.Custom.MULTIPLE]?.jsonPrimitive?.booleanOrNull ?: false + jsonObject[JsonSchema.Vocabularies.Custom.ATTRIBUTES]?.jsonObject?.let { attributes -> + builder.attributes.update(Json.decodeFromJsonElement(attributes)) + } + + return builder.build() + } + + /** + * Convert [ValueType] to JSON Schema type string + */ + private fun valueTypeToJsonType(valueType: ValueType): String = when (valueType) { + ValueType.NUMBER -> "number" + ValueType.STRING -> "string" + ValueType.BOOLEAN -> "boolean" + ValueType.LIST -> "array" + ValueType.NULL -> "null" + } + + /** + * Convert JSON Schema type string to [ValueType] + */ + private fun jsonTypeToValueType(type: String?): ValueType? = when (type) { + "number" -> ValueType.NUMBER + "string" -> ValueType.STRING + "boolean" -> ValueType.BOOLEAN + "array" -> ValueType.LIST + "null" -> ValueType.NULL + else -> null + } + + private fun List.mapToJsonTypes() = map { valueTypeToJsonType(it) } + + private fun Value.mapToJsonElement(): JsonElement = when (type) { + ValueType.NUMBER -> JsonPrimitive(number) + ValueType.STRING -> JsonPrimitive(string) + ValueType.BOOLEAN -> JsonPrimitive(boolean) + ValueType.LIST -> buildJsonArray { list.map { it.mapToJsonElement() }.forEach(::add) } + ValueType.NULL -> JsonNull + } + + private fun JsonElement.mapToValue(): Value = when (this) { + is JsonPrimitive -> when { + isString -> StringValue(content) + booleanOrNull != null -> if (boolean) True else False + doubleOrNull != null -> NumberValue(double) + intOrNull != null -> NumberValue(int) + longOrNull != null -> NumberValue(long) + else -> Null + } + is JsonArray -> ListValue(map { it.mapToValue() }) + is JsonObject -> { + val meta = MutableMeta() + forEach { (key, value) -> + meta[key] = value.mapToValue() + } + meta + } + JsonNull -> Null + } as Value +} + +/** + * Convert [MetaDescriptor] to a JSON Schema [JsonObject] + */ +public fun MetaDescriptor.toJsonSchema(): JsonObject { + return MetaDescriptorJsonSchemaConverter.convertMetaDescriptorToJsonSchema(this) +} + +/** + * Convert JSON Schema [JsonObject] to [MetaDescriptor] + */ +public fun JsonObject.toMetaDescriptor(): MetaDescriptor { + return MetaDescriptorJsonSchemaConverter.convertJsonSchemaToMetaDescriptor(this) +} diff --git a/dataforge-meta/src/commonTest/kotlin/space/kscience/dataforge/meta/descriptors/DescriptorJsonSchemaTest.kt b/dataforge-meta/src/commonTest/kotlin/space/kscience/dataforge/meta/descriptors/DescriptorJsonSchemaTest.kt new file mode 100644 index 00000000..2b89840d --- /dev/null +++ b/dataforge-meta/src/commonTest/kotlin/space/kscience/dataforge/meta/descriptors/DescriptorJsonSchemaTest.kt @@ -0,0 +1,66 @@ +package space.kscience.dataforge.meta.descriptors + +import io.github.optimumcode.json.schema.JsonSchema +import space.kscience.dataforge.meta.ValueType +import kotlin.test.Test +import kotlin.test.assertEquals + +class DescriptorJsonSchemaTest { + + val descriptor = MetaDescriptor { + node("aNode") { + description = "A root demo node" + value("b", ValueType.NUMBER) { + description = "b number value" + required() + } + node("otherNode") { + value("otherValue", ValueType.BOOLEAN) { + default(false) + description = "default value" + } + } + } + } + + @Test + fun testIsJsonSchemaValid() { + // Arrange + val descriptorJsonSchema = descriptor.toJsonSchema() + + // Act & Assert + JsonSchema.fromJsonElement(descriptorJsonSchema) + } + + @Test + fun testIsJsonSchemaConvertsToMetaDescriptor() { + // Arrange + val descriptorJsonSchema = descriptor.toJsonSchema() + + // Act & Assert + descriptorJsonSchema.toMetaDescriptor() + } + + @Test + fun testIsSerializationAndDeserializationWorksCorrect() { + // Arrange + val descriptorWithExplicitRequired = descriptor.applyRequiredRestrictions() + val descriptorJsonSchema = descriptorWithExplicitRequired.toJsonSchema() + + // Act + val descriptorFromJsonSchema = descriptorJsonSchema.toMetaDescriptor() + + // Assert + assertEquals(descriptorWithExplicitRequired, descriptorFromJsonSchema, "Expected equal descriptors") + } + + fun MetaDescriptor.applyRequiredRestrictions(): MetaDescriptor = + this.copy { + if (this@applyRequiredRestrictions.required) { + valueRestriction = ValueRestriction.REQUIRED + } + nodes.forEach { (name, childDescriptor) -> + node(name, childDescriptor.applyRequiredRestrictions()) + } + } +} \ No newline at end of file