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
24 changes: 24 additions & 0 deletions Sources/JSONSchema/Utilities/LockIsolated.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Foundation

/// A thread-safe wrapper that uses a lock to protect access to a mutable value.
///
/// This type provides a simple way to make a value safe to access from multiple threads
/// by wrapping it in a lock. Access to the value is controlled through the `withLock` method.
final class LockIsolated<Value>: @unchecked Sendable {
private var value: Value
private let lock = NSRecursiveLock()

init(_ value: @autoclosure @Sendable () throws -> Value) rethrows {
self.value = try value()
}

func withLock<T: Sendable>(
_ operation: @Sendable (inout Value) throws -> T
) rethrows -> T {
lock.lock()
defer { lock.unlock() }
var value = self.value
defer { self.value = value }
return try operation(&value)
}
}
76 changes: 62 additions & 14 deletions Sources/JSONSchema/Validation/Context.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,57 @@ import Foundation

/// Container for information used when validating a schema.
public final class Context: Sendable {
var dialect: Dialect
private let lockedDialect: LockIsolated<Dialect>
var dialect: Dialect {
get { lockedDialect.withLock { $0 } }
set { lockedDialect.withLock { $0 = newValue } }
}

var rootRawSchema: JSONValue?
private let lockedRootRawSchema: LockIsolated<JSONValue?>
var rootRawSchema: JSONValue? {
get { lockedRootRawSchema.withLock { $0 } }
set { lockedRootRawSchema.withLock { $0 = newValue } }
}

var identifierRegistry: [URL: JSONPointer] = [:]
private let lockedIdentifierRegistry: LockIsolated<[URL: JSONPointer]>
var identifierRegistry: [URL: JSONPointer] {
get { lockedIdentifierRegistry.withLock { $0 } }
set { lockedIdentifierRegistry.withLock { $0 = newValue } }
}

var remoteSchemaStorage: [String: JSONValue] = [:]
var schemaCache = [String: Schema]()
private let lockedRemoteSchemaStorage: LockIsolated<[String: JSONValue]>
var remoteSchemaStorage: [String: JSONValue] {
get { lockedRemoteSchemaStorage.withLock { $0 } }
set { lockedRemoteSchemaStorage.withLock { $0 = newValue } }
}

var anchors = [URL: JSONPointer]()
private let lockedSchemaCache: LockIsolated<[String: Schema]>
var schemaCache: [String: Schema] {
get { lockedSchemaCache.withLock { $0 } }
set { lockedSchemaCache.withLock { $0 = newValue } }
}

private let lockedAnchors: LockIsolated<[URL: JSONPointer]>
var anchors: [URL: JSONPointer] {
get { lockedAnchors.withLock { $0 } }
set { lockedAnchors.withLock { $0 = newValue } }
}

/// Stack of dynamic scopes used to resolve ``$dynamicRef`` references.
/// Each scope maps an anchor name to the schema location pointer and its
/// associated base URI.
var dynamicScopes: [[String: (pointer: JSONPointer, baseURI: URL)]] = []
private let lockedDynamicScopes: LockIsolated<[[String: (pointer: JSONPointer, baseURI: URL)]]>
var dynamicScopes: [[String: (pointer: JSONPointer, baseURI: URL)]] {
get { lockedDynamicScopes.withLock { $0 } }
set { lockedDynamicScopes.withLock { $0 = newValue } }
}

/// Validators used when the ``Keywords.Format`` keyword is present.
var formatValidators: [String: any FormatValidator] = [:]
private let lockedFormatValidators: LockIsolated<[String: any FormatValidator]>
var formatValidators: [String: any FormatValidator] {
get { lockedFormatValidators.withLock { $0 } }
set { lockedFormatValidators.withLock { $0 = newValue } }
}

/// A dictionary that tracks whether the `minContains` constraint is effectively zero
/// for specific schema locations.
Expand All @@ -30,25 +63,40 @@ public final class Context: Sendable {
/// at the specified schema location. A value of `true` means that the constraint
/// is effectively zero, allowing for validation to pass even if no instances match
/// the `contains` keyword.
var minContainsIsZero = [JSONPointer: Bool]()
private let lockedMinContainsIsZero: LockIsolated<[JSONPointer: Bool]>
var minContainsIsZero: [JSONPointer: Bool] {
get { lockedMinContainsIsZero.withLock { $0 } }
set { lockedMinContainsIsZero.withLock { $0 = newValue } }
}

/// A dictionary that stores the results of conditional validations within a schema.
///
/// - Key: A string representing the schema location pointer, excluding the specific
/// path to the "if", "else", or "then" conditions.
/// - Value: An optional `ValidationResult` that represents the outcome of the
/// conditional validation at the specified schema location.
var ifConditionalResults = [JSONPointer: ValidationResult]()
private let lockedIfConditionalResults: LockIsolated<[JSONPointer: ValidationResult]>
var ifConditionalResults: [JSONPointer: ValidationResult] {
get { lockedIfConditionalResults.withLock { $0 } }
set { lockedIfConditionalResults.withLock { $0 = newValue } }
}

public init(
dialect: Dialect,
remoteSchema: [String: JSONValue] = [:],
formatValidators: [any FormatValidator] = []
) {
self.dialect = dialect
self.remoteSchemaStorage = remoteSchema
self.formatValidators = Dictionary(
uniqueKeysWithValues: formatValidators.map { ($0.formatName, $0) }
self.lockedDialect = LockIsolated(dialect)
self.lockedRootRawSchema = LockIsolated(nil)
self.lockedIdentifierRegistry = LockIsolated([:])
self.lockedRemoteSchemaStorage = LockIsolated(remoteSchema)
self.lockedSchemaCache = LockIsolated([:])
self.lockedAnchors = LockIsolated([:])
self.lockedDynamicScopes = LockIsolated([])
self.lockedFormatValidators = LockIsolated(
Dictionary(uniqueKeysWithValues: formatValidators.map { ($0.formatName, $0) })
)
self.lockedMinContainsIsZero = LockIsolated([:])
self.lockedIfConditionalResults = LockIsolated([:])
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import JSONSchema
}
}

public protocol PropertyCollection: Sendable {
public protocol PropertyCollection {
associatedtype Output

var schemaValue: SchemaValue { get }
Expand Down
4 changes: 2 additions & 2 deletions Sources/JSONSchemaBuilder/JSONComponent/JSONSchema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public struct JSONSchema<Components: JSONSchemaComponent, NewOutput>: JSONSchema
set { components.schemaValue = newValue }
}

let transform: @Sendable (Components.Output) -> NewOutput
let transform: (Components.Output) -> NewOutput

var components: Components

Expand All @@ -17,7 +17,7 @@ public struct JSONSchema<Components: JSONSchemaComponent, NewOutput>: JSONSchema
/// - transform: The transform to apply to the output.
/// - component: The components to group together.
public init(
_ transform: @Sendable @escaping (Components.Output) -> NewOutput,
_ transform: @escaping (Components.Output) -> NewOutput,
@JSONSchemaBuilder component: () -> Components
) {
self.transform = transform
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import Foundation
import JSONSchema

/// A component for use in ``JSONSchemaBuilder`` to build, annotate, and validate schemas.
public protocol JSONSchemaComponent<Output>: Sendable {
public protocol JSONSchemaComponent<Output> {
associatedtype Output

var schemaValue: SchemaValue { get set }

/// Parse a JSON instance into a Swift type using the schema.
/// - Parameter value: The value (aka instance or document) to validate.
/// - Returns: A validated output or error messages.
@Sendable func parse(_ value: JSONValue) -> Parsed<Output, ParseIssue>
func parse(_ value: JSONValue) -> Parsed<Output, ParseIssue>
}

extension JSONSchemaComponent {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ extension JSONSchemaComponent {
extension JSONComponents {
/// Component for type erasure.
public struct AnySchemaComponent<Output>: JSONSchemaComponent {
private let validate: @Sendable (JSONValue) -> Parsed<Output, ParseIssue>
private let validate: (JSONValue) -> Parsed<Output, ParseIssue>
public var schemaValue: SchemaValue

public init<Component: JSONSchemaComponent>(_ component: Component)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import JSONSchema

/// A component that represents a JSON property.
/// Used in the ``JSONObject/init(with:)`` initializer to define the properties of an object schema.
public protocol JSONPropertyComponent: Sendable {
public protocol JSONPropertyComponent {
associatedtype Value: JSONSchemaComponent
associatedtype Output

Expand Down