diff --git a/Sources/PoieticCore/Constraints/Constraint+Edge.swift b/Sources/PoieticCore/Constraints/Constraint+Edge.swift index 3dd65ea7..c076db60 100644 --- a/Sources/PoieticCore/Constraints/Constraint+Edge.swift +++ b/Sources/PoieticCore/Constraints/Constraint+Edge.swift @@ -39,7 +39,7 @@ public final class EdgeEndpointRequirement: ConstraintRequirement { self.edge = edge } - public func check(frame: some Frame, objects: [any ObjectSnapshot]) -> [ObjectID] { + public func check(frame: some Frame, objects: [some ObjectSnapshot]) -> [ObjectID] { var violations: [ObjectID] = [] for object in objects { @@ -49,19 +49,19 @@ public final class EdgeEndpointRequirement: ConstraintRequirement { if let predicate = self.origin { let node = frame.node(edge.origin) - if !predicate.match(frame: frame, object: node) { + if !predicate.match(node, in: frame) { violations.append(edge.id) continue } } if let predicate = self.target { let node = frame.node(edge.target) - if !predicate.match(frame: frame, object: node) { + if !predicate.match(node, in: frame) { violations.append(edge.id) continue } } - if let predicate = self.edge, !predicate.match(frame: frame, object: edge.snapshot) { + if let predicate = self.edge, !predicate.match(edge.snapshot, in: frame) { violations.append(edge.id) continue } diff --git a/Sources/PoieticCore/Constraints/Constraint+Node.swift b/Sources/PoieticCore/Constraints/Constraint+Node.swift index 48e6b7b5..05d3658f 100644 --- a/Sources/PoieticCore/Constraints/Constraint+Node.swift +++ b/Sources/PoieticCore/Constraints/Constraint+Node.swift @@ -5,9 +5,17 @@ // Created by Stefan Urbanek on 16/06/2022. // +/// Requirement that there must be at most one edge adjacent to a tested node. +/// public final class UniqueNeighbourRequirement: ConstraintRequirement { + /// Predicate to test the adjacent edges. public let predicate: Predicate + + /// Direction of the edge relative to the node being tested for the requirement. public let direction: EdgeDirection + + /// Flag whether at least one edge is required. If true, then the edge matching + /// the predicate must exist. public let isRequired: Bool /// Creates a constraint for unique neighbour. @@ -18,8 +26,8 @@ public final class UniqueNeighbourRequirement: ConstraintRequirement { /// one neighbour or when there is none. /// /// - Parameters: - /// - selector: neigborhood selector that has to be unique for the - /// matching node + /// - predicate: Predicate to select neighbourhood edges. + /// - direction: Edge direction to consider relative to the object tested. /// - required: Wether the unique neighbour is required. /// public init(_ predicate: Predicate, direction: EdgeDirection = .outgoing, required: Bool=false) { @@ -27,7 +35,6 @@ public final class UniqueNeighbourRequirement: ConstraintRequirement { self.direction = direction self.isRequired = required } - public func check(frame: some Frame, objects: [any ObjectSnapshot]) -> [ObjectID] { return objects.filter { @@ -35,7 +42,7 @@ public final class UniqueNeighbourRequirement: ConstraintRequirement { return false } let hood = frame.hood($0.id, direction: direction) { edge in - predicate.match(frame: frame, object: edge.snapshot) + predicate.match(edge.snapshot, in: frame) } let count = hood.edges.count diff --git a/Sources/PoieticCore/Constraints/Constraint.swift b/Sources/PoieticCore/Constraints/Constraint.swift index 5eb9b278..1ee7dc4e 100644 --- a/Sources/PoieticCore/Constraints/Constraint.swift +++ b/Sources/PoieticCore/Constraints/Constraint.swift @@ -58,7 +58,6 @@ public struct ConstraintViolation: Error, CustomDebugStringConvertible { /// ) /// ) /// ``` - public final class Constraint: Sendable { /// Identifier of the constraint. /// @@ -110,9 +109,9 @@ public final class Constraint: Sendable { /// Check the frame for the constraint and return a list of nodes that /// violate the constraint /// - public func check(_ frame: any Frame) -> [ObjectID] { + public func check(_ frame: some Frame) -> [ObjectID] { let matched = frame.snapshots.filter { - match.match(frame: frame, object: $0) + match.match($0, in: frame) } // .map { $0.snapshot } return requirement.check(frame: frame, objects: matched) @@ -123,7 +122,7 @@ public final class Constraint: Sendable { /// public protocol ConstraintRequirement: Sendable { /// - Returns: List of IDs of objects that do not satisfy the requirement. - func check(frame: some Frame, objects: [any ObjectSnapshot]) -> [ObjectID] + func check(frame: some Frame, objects: [some ObjectSnapshot]) -> [ObjectID] } /// Requirement that all matched objects satisfy a given predicate. @@ -137,8 +136,8 @@ public final class AllSatisfy: ConstraintRequirement { self.predicate = predicate } - public func check(frame: some Frame, objects: [any ObjectSnapshot]) -> [ObjectID] { - objects.filter { !predicate.match(frame: frame, object: $0) } + public func check(frame: some Frame, objects: [some ObjectSnapshot]) -> [ObjectID] { + objects.filter { !predicate.match($0, in: frame) } .map { $0.id } } } @@ -157,7 +156,7 @@ public final class RejectAll: ConstraintRequirement { /// Returns all objects it is provided – meaning, that all of them are /// violating the constraint. /// - public func check(frame: some Frame, objects: [any ObjectSnapshot]) -> [ObjectID] { + public func check(frame: some Frame, objects: [some ObjectSnapshot]) -> [ObjectID] { /// We reject whatever comes in return objects.map { $0.id } } @@ -176,7 +175,7 @@ public final class AcceptAll: ConstraintRequirement { /// Returns an empty list, meaning that none of the objects are violating /// the constraint. /// - public func check(frame: some Frame, objects: [any ObjectSnapshot]) -> [ObjectID] { + public func check(frame: some Frame, objects: [some ObjectSnapshot]) -> [ObjectID] { // We accept everything, therefore we do not return any violations. return [] } @@ -201,7 +200,7 @@ public final class UniqueProperty: ConstraintRequirement { /// value from each of the objects and returns a list of those objects /// that have duplicate values. /// - public func check(frame: some Frame, objects: [any ObjectSnapshot]) -> [ObjectID] { + public func check(frame: some Frame, objects: [some ObjectSnapshot]) -> [ObjectID] { var seen: [Variant:[ObjectID]] = [:] for object in objects { @@ -211,11 +210,7 @@ public final class UniqueProperty: ConstraintRequirement { seen[value, default: []].append(object.id) } - let duplicates = seen.filter { - $0.value.count > 1 - }.flatMap { - $0.value - } + let duplicates = seen.filter { $0.value.count > 1 }.flatMap { $0.value } return duplicates } } diff --git a/Sources/PoieticCore/Design/Errors.swift b/Sources/PoieticCore/Design/Errors.swift deleted file mode 100644 index 6b32b108..00000000 --- a/Sources/PoieticCore/Design/Errors.swift +++ /dev/null @@ -1,7 +0,0 @@ -// -// File.swift -// -// -// Created by Stefan Urbanek on 19/04/2024. -// - diff --git a/Sources/PoieticCore/Design/Frame.swift b/Sources/PoieticCore/Design/Frame.swift index 83ec4b11..dfc0d99c 100644 --- a/Sources/PoieticCore/Design/Frame.swift +++ b/Sources/PoieticCore/Design/Frame.swift @@ -236,7 +236,7 @@ extension Frame { public func filter(_ predicate: Predicate) -> [Snapshot] { return snapshots.filter { - predicate.match(frame: self, object: $0) + predicate.match($0, in: self) } } } diff --git a/Sources/PoieticCore/Design/TransientObject.swift b/Sources/PoieticCore/Design/TransientObject.swift index 78ef46ff..4b2bd6e8 100644 --- a/Sources/PoieticCore/Design/TransientObject.swift +++ b/Sources/PoieticCore/Design/TransientObject.swift @@ -14,8 +14,17 @@ public let ReservedAttributeNames = [ ] +/// Transient object that can be modified. +/// +/// Mutable objects are temporary and typically exist only during a change +/// transaction. New objects are created within a ``TransientFrame`` by ``TransientFrame/create(_:id:snapshotID:structure:parent:children:attributes:components:)``. +/// Mutable versions of existing stable objects are created with``TransientFrame/mutate(_:)``. +/// +/// Mutable objects are converted to stable objects with ``TransientFrame/accept()``. +/// +/// - SeeAlso: ``TransientFrame`` +/// public class MutableObject: ObjectSnapshot { - public let id: ObjectID public let snapshotID: ObjectID public let type: ObjectType @@ -120,11 +129,13 @@ public class MutableObject: ObjectSnapshot { // MARK: - Transient Object -/// A proxy for for an object in a transient frame. +/// A wrapper for for an object in a transient frame. /// /// The transient object refers to one of two possible target objects: original /// stable snapshot or a mutable snapshot in a ``TransientFrame``. /// +/// - SeeAlso: ``MutableObject`` +/// public struct TransientObject: ObjectSnapshot { let frame: TransientFrame public let id: ObjectID diff --git a/Sources/PoieticCore/Predicate/Predicate+Edge.swift b/Sources/PoieticCore/Predicate/Predicate+Edge.swift index de67c3c0..b05bdd24 100644 --- a/Sources/PoieticCore/Predicate/Predicate+Edge.swift +++ b/Sources/PoieticCore/Predicate/Predicate+Edge.swift @@ -29,24 +29,24 @@ public final class EdgePredicate: Predicate { self.edgePredicate = edge } - public func match(frame: some Frame, object: some ObjectSnapshot) -> Bool { + public func match(_ object: some ObjectSnapshot, in frame: some Frame) -> Bool { guard let edge = EdgeSnapshot(object) else { return false } if let predicate = originPredicate { let node = frame.node(edge.origin) - if !predicate.match(frame: frame, object: node) { + if !predicate.match(node, in: frame) { return false } } if let predicate = targetPredicate { let node = frame.node(edge.target) - if !predicate.match(frame: frame, object: node) { + if !predicate.match(node, in: frame) { return false } } if let predicate = edgePredicate { - if !predicate.match(frame: frame, object: edge.snapshot) { + if !predicate.match(edge.snapshot, in: frame) { return false } } diff --git a/Sources/PoieticCore/Predicate/Predicate.swift b/Sources/PoieticCore/Predicate/Predicate.swift index 2199bf28..91b8135d 100644 --- a/Sources/PoieticCore/Predicate/Predicate.swift +++ b/Sources/PoieticCore/Predicate/Predicate.swift @@ -1,21 +1,36 @@ // -// File.swift +// Predicate.swift // // // Created by Stefan Urbanek on 13/06/2022. // -public enum LogicalConnective: Sendable { - case and - case or -} - -/// A predicate. +/// An object predicate. +/// +/// Predicates check properties of an object using the ``match(frame:object:)`` method. +/// +/// Predicates can be composed using logical operations ``and(_:)`` and ``or(_:)``. For example: +/// +/// ```swift +/// let predicate = IsTypePredicate(ObjectType.Auxiliary) +/// .or(IsTypePredicate(ObjectType.Stock)) +/// ``` +/// +/// - Note: When adding a new predicate type, please consider its implementability +/// by other, foreign systems. /// public protocol Predicate: Sendable { - func match(frame: some Frame, object: some ObjectSnapshot) -> Bool + /// Check whether an object matches the predicate condition. + /// + func match(_ object: some ObjectSnapshot, in frame: some Frame) -> Bool + + /// Creates a compound predicate with the other predicate using a logical ∧ – `and` connective. func and(_ predicate: Predicate) -> CompoundPredicate + + /// Creates a compound predicate with the other predicate using a logical ⋁ – `or` connective. func or(_ predicate: Predicate) -> CompoundPredicate + + /// Creates a predicate that is a negation of the receiver. func not() -> Predicate } @@ -33,81 +48,123 @@ extension Predicate { // TODO: Add &&, || and ! operators -public final class CompoundPredicate: Predicate { +/// Type of logical connective for a compound predicate. +/// +/// - SeeAlso: ``CompoundPredicate`` +/// +public enum LogicalConnective: Sendable { + /// Logical ∧ – `and` connective. + /// + /// - SeeAlso: ``CompoundPredicate/and(_:)`` + case and + /// Logical ⋁ – `or` connective. + /// + /// - SeeAlso: ``CompoundPredicate/or(_:)`` + case or +} + +/// Predicate that connects multiple predicates with a logical connective. +/// +/// - SeeAlso: ``Predicate/and(_:)``, ``Predicate/or(_:)`` +/// +public struct CompoundPredicate: Predicate { + /// Logical connective to connect the predicates with. public let connective: LogicalConnective + + /// List of predicates that are evaluated together with the same logical connective. public let predicates: [Predicate] + /// Create a new compound predicate. + /// + /// - Parementes: + /// - connective: Logical connective to connect all the provided predicates with. + /// - predicates: List of predicates to connect. + /// public init(_ connective: LogicalConnective, predicates: any Predicate...) { self.connective = connective self.predicates = predicates } - public func match(frame: some Frame, object: some ObjectSnapshot) -> Bool { + public func match(_ object: some ObjectSnapshot, in frame: some Frame) -> Bool { switch connective { - case .and: return predicates.allSatisfy{ $0.match(frame: frame, object: object) } - case .or: return predicates.contains{ $0.match(frame: frame, object: object) } + case .and: return predicates.allSatisfy{ $0.match(object, in: frame) } + case .or: return predicates.contains{ $0.match(object, in: frame) } } } } -public final class NegationPredicate: Predicate { +public struct NegationPredicate: Predicate { public let predicate: Predicate public init(_ predicate: any Predicate) { self.predicate = predicate } - public func match(frame: some Frame, object: some ObjectSnapshot) -> Bool { - return !predicate.match(frame: frame, object: object) + public func match(_ object: some ObjectSnapshot, in frame: some Frame) -> Bool { + return !predicate.match(object, in: frame) } } /// Predicate that matches any object. /// -public final class AnyPredicate: Predicate { +public struct AnyPredicate: Predicate { public init() {} /// Matches any node – always returns `true`. /// - public func match(frame: some Frame, object: some ObjectSnapshot) -> Bool { + public func match(_ object: some ObjectSnapshot, in frame: some Frame) -> Bool { return true } } -public final class HasComponentPredicate: Predicate { +/// Predicate to test whether an object has a given trait. +/// +public struct HasComponentPredicate: Predicate { + /// Component to be tested for. let type: Component.Type + /// Create a new predicate to test for a component. public init(_ type: Component.Type) { self.type = type } - public func match(frame: some Frame, object: some ObjectSnapshot) -> Bool { + public func match(_ object: some ObjectSnapshot, in frame: some Frame) -> Bool { return object.components.has(self.type) } } -public final class HasTraitPredicate: Predicate { +/// Predicate to test whether an object has a given trait. +/// +public struct HasTraitPredicate: Predicate { + /// Trait to be tested for. let trait: Trait + /// Create a new predicate to test for a trait. public init(_ trait: Trait) { self.trait = trait } - public func match(frame: some Frame, object: some ObjectSnapshot) -> Bool { + public func match(_ object: some ObjectSnapshot, in frame: some Frame) -> Bool { object.type.traits.contains { $0 === trait } } } - -public final class IsTypePredicate: Predicate { +/// Predicate to test whether an object is of one or multiple given types. +/// +public struct IsTypePredicate: Predicate { + /// List of types to test for. let types: [ObjectType] + /// Create a new predicate with a list of types to test for. public init(_ types: [ObjectType]) { self.types = types } + + /// Create a new predicate with a type to test for. public init(_ type: ObjectType) { self.types = [type] } - public func match(frame: some Frame, object: some ObjectSnapshot) -> Bool { + + public func match(_ object: some ObjectSnapshot, in frame: some Frame) -> Bool { return types.allSatisfy{ object.type === $0 } diff --git a/Tests/PoieticCoreTests/PredicateTests.swift b/Tests/PoieticCoreTests/PredicateTests.swift new file mode 100644 index 00000000..912cb0f4 --- /dev/null +++ b/Tests/PoieticCoreTests/PredicateTests.swift @@ -0,0 +1,47 @@ +// +// PredicateTests.swift +// poietic-core +// +// Created by Stefan Urbanek on 13/11/2024. +// + +import XCTest +@testable import PoieticCore + +final class TestPredicates : XCTestCase { + var design: Design! + var frame: StableFrame! + var empty: StableObject! + var textObject: StableObject! + + override func setUp() { + design = Design() + empty = StableObject(id: design.allocateID(), + snapshotID: design.allocateID(), + type: TestType) + textObject = StableObject(id: design.allocateID(), + snapshotID: design.allocateID(), + type: TestTypeWithDefault) + frame = StableFrame(design: design, + id: design.allocateID(), + snapshots: [empty, textObject] + ) + } + func testAnyPredicate() throws { + let predicate = AnyPredicate() + XCTAssertTrue(predicate.match(empty, in: frame)) + } + func testNotPredicate() throws { + let predicate = NegationPredicate(AnyPredicate()) + XCTAssertFalse(predicate.match(empty, in: frame)) + } + func testTypePredicate() throws { + XCTAssertTrue(IsTypePredicate(TestType).match(empty, in: frame)) + XCTAssertFalse(IsTypePredicate(TestEdgeType).match(empty, in: frame)) + } + func testTraitPredicate() throws { + XCTAssertTrue(HasTraitPredicate(TestTraitWithDefault).match(textObject, in: frame)) + XCTAssertFalse(HasTraitPredicate(TestTraitNoDefault).match(textObject, in: frame)) + } +} +