-
Notifications
You must be signed in to change notification settings - Fork 178
Here's a refactoring of the JSON Schema code to enhance clarity and maintainability. #450
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -2,7 +2,19 @@ package wu.seal.jsontokotlin.model.jsonschema | |||||||||||||||
|
||||||||||||||||
import com.google.gson.annotations.SerializedName | ||||||||||||||||
|
||||||||||||||||
// See specification: https://json-schema.org/understanding-json-schema/reference/object.html | ||||||||||||||||
/** | ||||||||||||||||
* Represents a JSON schema document. | ||||||||||||||||
* | ||||||||||||||||
* This class models the structure of a JSON schema, allowing for parsing and resolving | ||||||||||||||||
* references (`$ref`) within the schema. It supports standard schema keywords like | ||||||||||||||||
* `definitions`, `$defs`, and `properties`. | ||||||||||||||||
* | ||||||||||||||||
* See JSON Schema Specification: [https://json-schema.org/understanding-json-schema/](https://json-schema.org/understanding-json-schema/) | ||||||||||||||||
* | ||||||||||||||||
* @property schema The value of the `\$schema` keyword, indicating the schema dialect. | ||||||||||||||||
* @property definitions A map of definitions, primarily used in older JSON schema versions. | ||||||||||||||||
* @property defs A map of definitions, introduced in Draft 2019-09 as a replacement for `definitions`. | ||||||||||||||||
*/ | ||||||||||||||||
class JsonSchema( | ||||||||||||||||
@SerializedName("\$schema") | ||||||||||||||||
val schema: String? = null, | ||||||||||||||||
|
@@ -14,47 +26,106 @@ class JsonSchema( | |||||||||||||||
// Get a combined map of both definitions and $defs | ||||||||||||||||
// This allows backward compatibility with older schema versions | ||||||||||||||||
private val allDefinitions: Map<String, PropertyDef> | ||||||||||||||||
get() { | ||||||||||||||||
val combinedMap = definitions.toMutableMap() | ||||||||||||||||
defs?.let { combinedMap.putAll(it) } | ||||||||||||||||
return combinedMap | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
//See: https://json-schema.org/understanding-json-schema/structuring.html | ||||||||||||||||
get() = definitions + (defs ?: emptyMap()) | ||||||||||||||||
|
||||||||||||||||
/** | ||||||||||||||||
* Resolves a JSON Pointer reference (`$ref`) to a [PropertyDef] within this schema. | ||||||||||||||||
* | ||||||||||||||||
* Currently, only local references (starting with `#`) are supported. | ||||||||||||||||
* Examples of supported $ref formats: | ||||||||||||||||
* - `#/definitions/MyType` | ||||||||||||||||
* - `#/\$defs/AnotherType` | ||||||||||||||||
* - `#/properties/user/properties/address` | ||||||||||||||||
* - `#MyObject` (if `MyObject` is an id of a definition at the root) | ||||||||||||||||
* | ||||||||||||||||
* See JSON Schema structuring: [https://json-schema.org/understanding-json-schema/structuring.html](https://json-schema.org/understanding-json-schema/structuring.html) | ||||||||||||||||
* | ||||||||||||||||
* @param ref The JSON Pointer reference string (e.g., `#/definitions/User`). | ||||||||||||||||
* @return The resolved [PropertyDef]. | ||||||||||||||||
* @throws IllegalArgumentException if the `ref` string is malformed (e.g., too short). | ||||||||||||||||
* @throws NotImplementedError if the `ref` points to a non-local definition (doesn't start with '#'). | ||||||||||||||||
* @throws ClassNotFoundException if the definition pointed to by `ref` cannot be found. | ||||||||||||||||
*/ | ||||||||||||||||
fun resolveDefinition(ref: String): PropertyDef { | ||||||||||||||||
if (ref.length < 2) throw IllegalArgumentException("Bad ref: $ref") | ||||||||||||||||
if (!ref.startsWith("#")) throw NotImplementedError("Not local definitions are not supported (ref: $ref)") | ||||||||||||||||
|
||||||||||||||||
return resolveLocalDefinition(ref) | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
/** | ||||||||||||||||
* Resolves a local definition path (starting with '#'). | ||||||||||||||||
* @param ref The definition reference string. | ||||||||||||||||
* @return The resolved [PropertyDef]. | ||||||||||||||||
* @throws ClassNotFoundException if the definition is not found. | ||||||||||||||||
* @throws NotImplementedError if the path structure is not supported. | ||||||||||||||||
* @throws IllegalArgumentException if the path contains unknown components. | ||||||||||||||||
*/ | ||||||||||||||||
private fun resolveLocalDefinition(ref: String): PropertyDef { | ||||||||||||||||
val path = ref.split('/') | ||||||||||||||||
return when { | ||||||||||||||||
path.count() == 1 -> allDefinitions.values.firstOrNull { it.id == path[0] } | ||||||||||||||||
?: throw ClassNotFoundException("Definition $ref not found") | ||||||||||||||||
path[1] == "definitions" -> definitions[path[2]] | ||||||||||||||||
?: throw ClassNotFoundException("Definition $ref not found") | ||||||||||||||||
path[1] == "\$defs" -> defs?.get(path[2]) | ||||||||||||||||
path.count() == 1 -> allDefinitions.values.firstOrNull { it.id == path[0] } // TODO: This could be path[0].substring(1) if # is always present | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When matching a single-segment ref, strip the leading '#' (e.g., use
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||||||||||
?: throw ClassNotFoundException("Definition $ref not found") | ||||||||||||||||
Comment on lines
+67
to
68
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove or resolve this TODO before merging to avoid leaving outdated comments in production code.
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||||||||||
path[1] == "definitions" -> findDefinitionInMaps(path[2], definitions, ref, "definitions") | ||||||||||||||||
path[1] == "\$defs" -> findDefinitionInMaps(path[2], defs, ref, "\$defs") | ||||||||||||||||
path[1] == "properties" -> { | ||||||||||||||||
var property: PropertyDef = properties?.get(path[2]) | ||||||||||||||||
?: throw ClassNotFoundException("Definition $ref not found") | ||||||||||||||||
val iterator = path.subList(3, path.count()).iterator() | ||||||||||||||||
do { | ||||||||||||||||
val next = iterator.next() | ||||||||||||||||
property = when (next) { | ||||||||||||||||
"properties" -> { | ||||||||||||||||
val propName = iterator.next() | ||||||||||||||||
property.properties?.get(propName) | ||||||||||||||||
?: throw ClassNotFoundException("Definition $propName not found at path $ref") | ||||||||||||||||
} | ||||||||||||||||
"items" -> property.items ?: throw ClassNotFoundException("Definition $next not found at path $ref") | ||||||||||||||||
else -> throw IllegalArgumentException("Unknown json-object property $next not found at path $ref") | ||||||||||||||||
} | ||||||||||||||||
} while (iterator.hasNext()) | ||||||||||||||||
|
||||||||||||||||
property | ||||||||||||||||
val initialProperty = properties?.get(path[2]) | ||||||||||||||||
?: throw ClassNotFoundException("Definition '${path[2]}' not found in properties at path $ref") | ||||||||||||||||
findPropertyRecursive(initialProperty, path.subList(3, path.count()).iterator(), ref) | ||||||||||||||||
} | ||||||||||||||||
else -> throw NotImplementedError("Cannot resolve ref path: $ref") | ||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
/** | ||||||||||||||||
* Finds a definition in the provided map. | ||||||||||||||||
* @param defName The name of the definition to find. | ||||||||||||||||
* @param map The map to search in (can be nullable, e.g. `defs`). | ||||||||||||||||
* @param originalRef The original reference string (for error reporting). | ||||||||||||||||
* @param mapName The name of the map being searched (for error reporting). | ||||||||||||||||
* @return The resolved [PropertyDef]. | ||||||||||||||||
* @throws ClassNotFoundException if the definition is not found in the map. | ||||||||||||||||
*/ | ||||||||||||||||
private fun findDefinitionInMaps( | ||||||||||||||||
defName: String, | ||||||||||||||||
map: Map<String, PropertyDef>?, | ||||||||||||||||
originalRef: String, | ||||||||||||||||
mapName: String | ||||||||||||||||
): PropertyDef { | ||||||||||||||||
return map?.get(defName) | ||||||||||||||||
?: throw ClassNotFoundException("Definition '$defName' not found in '$mapName' at path $originalRef") | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
/** | ||||||||||||||||
* Recursively traverses properties based on the path segments. | ||||||||||||||||
* @param currentProperty The current [PropertyDef] being inspected. | ||||||||||||||||
* @param pathIterator An iterator for the remaining path segments. | ||||||||||||||||
* @param originalRef The original reference string (for error reporting). | ||||||||||||||||
* @return The resolved [PropertyDef]. | ||||||||||||||||
* @throws ClassNotFoundException if a segment in the path is not found. | ||||||||||||||||
* @throws IllegalArgumentException if an unknown path segment is encountered. | ||||||||||||||||
*/ | ||||||||||||||||
private fun findPropertyRecursive( | ||||||||||||||||
currentProperty: PropertyDef, | ||||||||||||||||
pathIterator: Iterator<String>, | ||||||||||||||||
originalRef: String | ||||||||||||||||
): PropertyDef { | ||||||||||||||||
var property = currentProperty | ||||||||||||||||
while (pathIterator.hasNext()) { | ||||||||||||||||
val segment = pathIterator.next() | ||||||||||||||||
property = when (segment) { | ||||||||||||||||
"properties" -> { | ||||||||||||||||
if (!pathIterator.hasNext()) throw IllegalArgumentException("Missing property name after 'properties' in $originalRef") | ||||||||||||||||
val propName = pathIterator.next() | ||||||||||||||||
property.properties?.get(propName) | ||||||||||||||||
?: throw ClassNotFoundException("Property '$propName' not found under '${property.id ?: "unknown"}' at path $originalRef") | ||||||||||||||||
} | ||||||||||||||||
"items" -> property.items | ||||||||||||||||
?: throw ClassNotFoundException("Property 'items' not found under '${property.id ?: "unknown"}' at path $originalRef") | ||||||||||||||||
else -> throw IllegalArgumentException("Unknown json-object property '$segment' in path $originalRef") | ||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
return property | ||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use a more general
List<*>
check instead ofArrayList<*>
to support other collection implementations.Copilot uses AI. Check for mistakes.