Skip to content

Commit

Permalink
Now require all values used in ForkedResource to be Equatable.
Browse files Browse the repository at this point in the history
Usually they are, being value types. It makes the logic and expectations much simpler if this is required.
  • Loading branch information
drewmccormack committed Dec 18, 2024
1 parent daa1082 commit 38645de
Show file tree
Hide file tree
Showing 18 changed files with 25 additions and 71 deletions.
5 changes: 1 addition & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,7 @@ The `@Merged` attribute tells `ForkedModel` that the property is `Mergeable`, an

If you have a custom `Mergeable` type, like `AccumulatingInt`, applying `@Merged` will cause it to merge using the `merged(withSubordinate:commonAncestor:)` method you provided.

Properties without `@Merged` attached will be merged atomically, with a more recent change taking precedence over an older one.

- Properties that are `Equatable` will be merged property-wise, independent of the rest of the struct, based on the most recent change to the property itself
- Properties that are not `Equatable` will take their value from the newest value of the struct, and not be merged property-wise
Properties without `@Merged` attached will be merged atomically, with a more recent change taking precedence over an older one. Properties will be merged in a property-wise manner, based on the most recent change to the property itself

## Sample Code

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation
import Forked

struct AccumulatingInt: Mergeable {
struct AccumulatingInt: Mergeable, Equatable {
var value: Int
func merged(withSubordinate other: AccumulatingInt, commonAncestor: AccumulatingInt) throws -> AccumulatingInt {
AccumulatingInt(value: value + other.value - commonAncestor.value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ extension Fork {
static let ui = Fork(name: "ui")
}

struct Model: Codable {
struct Model: Codable, Mergeable {
var text: String
func merged(withSubordinate other: Model, commonAncestor: Model) throws -> Model { self }
}

@MainActor
Expand Down
2 changes: 1 addition & 1 deletion Sources/Forked/AtomicRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Foundation
/// also `Codable`, and can be converted to a serialized form and saved as a file.
/// Saving and loading are atomic, that is, the whole repository is loaded from file, and the whole
/// file is written to disk.
public final class AtomicRepository<Resource>: Repository {
public final class AtomicRepository<Resource: Equatable>: Repository {
private var forkToResource: [Fork:[Commit<Resource>]] = [:]

/// If set, the persistence of the repo is managed for you. It will load and
Expand Down
16 changes: 3 additions & 13 deletions Sources/Forked/Commit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Foundation

/// A wrapper to hold the resource. This allows for the resource to be
/// absent in a fork, similar to using `nil`.
public enum CommitContent<Resource>: Equatable {
public enum CommitContent<Resource: Equatable> {
/// The content is not present. Perhaps it has not been added yet,
/// or it may have been removed.
case none
Expand All @@ -16,20 +16,10 @@ public enum CommitContent<Resource>: Equatable {
}
return nil
}

/// By default, resource content is treated as not being equal if we can't test it.
public static func == (lhs: CommitContent<Resource>, rhs: CommitContent<Resource>) -> Bool {
switch (lhs, rhs) {
case (.none, .none):
return true
default:
return false
}
}
}

extension CommitContent: Codable where Resource: Codable {}
extension CommitContent where Resource: Equatable {
extension CommitContent: Equatable {

/// The resource is Equatable, so test explicitly for equality.
public static func == (lhs: CommitContent<Resource>, rhs: CommitContent<Resource>) -> Bool {
Expand All @@ -47,7 +37,7 @@ extension CommitContent where Resource: Equatable {

/// A commit comprises of content, which is usually a value of the stored resource,
/// together with a `Version`.
public struct Commit<Resource>: Hashable, Equatable {
public struct Commit<Resource: Equatable>: Hashable, Equatable {
/// The content stored in the commit, usually a copy of the resource.
public var content: CommitContent<Resource>

Expand Down
2 changes: 1 addition & 1 deletion Sources/Forked/ConflictingCommits.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

internal struct ConflictingCommits<Resource> {
internal struct ConflictingCommits<Resource: Equatable> {
public var dominant: Commit<Resource>
public var subordinate: Commit<Resource>

Expand Down
2 changes: 1 addition & 1 deletion Sources/Forked/ForkOccupation.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation

/// Different versions of the resource currently stored in a particular fork.
internal enum ForkOccupation<Resource>: Equatable {
internal enum ForkOccupation<Resource: Equatable>: Equatable {
case sameAsMain
case leftBehindByMain(Commit<Resource>)
case aheadOrConflictingWithMain(Commit<Resource>, commonAncestor: Commit<Resource>)
Expand Down
4 changes: 2 additions & 2 deletions Sources/Forked/Mergeable.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import Foundation

public protocol Mergeable {
public protocol Mergeable: Equatable {
func merged(withSubordinate other: Self, commonAncestor: Self) throws -> Self
}

extension Optional: Mergeable where Wrapped: Mergeable & Equatable {
extension Optional: Mergeable where Wrapped: Mergeable {

public func merged(withSubordinate other: Self, commonAncestor: Self) throws -> Self {
if self == commonAncestor {
Expand Down
2 changes: 1 addition & 1 deletion Sources/Forked/QuickFork.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

public typealias QuickFork<T> = ForkedResource<AtomicRepository<T>>
public typealias QuickFork<T: Equatable> = ForkedResource<AtomicRepository<T>>

public extension QuickFork {

Expand Down
2 changes: 1 addition & 1 deletion Sources/Forked/Repository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Foundation
/// Classes conforming to this type simply have to setup a storage
/// mechanism, and handle the requests, keeping commits assigned to forks.
public protocol Repository: AnyObject {
associatedtype Resource
associatedtype Resource: Equatable

/// The forks in the repository, including .main, in no particular order.
var forks: [Fork] { get }
Expand Down
7 changes: 2 additions & 5 deletions Sources/ForkedMerge/MergableTypes/MergeableDictionary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import Forked

/// Represents a mergable type for a dictionary of values.
/// Uses a CRDT algorithm.
public struct MergeableDictionary<Key, Value> where Key: Hashable, Value: Equatable {
public struct MergeableDictionary<Key:Hashable, Value: Equatable>: Equatable {

fileprivate struct ValueContainer {
fileprivate struct ValueContainer: Equatable {
var isDeleted: Bool
var timestamp: StableTimestamp
var value: Value
Expand Down Expand Up @@ -166,8 +166,5 @@ extension MergeableDictionary where Value: Mergeable {
extension MergeableDictionary: Codable where Value: Codable, Key: Codable {}
extension MergeableDictionary.ValueContainer: Codable where Value: Codable, Key: Codable {}

extension MergeableDictionary: Equatable where Value: Equatable {}
extension MergeableDictionary.ValueContainer: Equatable where Value: Equatable {}

extension MergeableDictionary: Hashable where Value: Hashable {}
extension MergeableDictionary.ValueContainer: Hashable where Value: Hashable {}
10 changes: 2 additions & 8 deletions Sources/ForkedMerge/MergableTypes/MergeableSet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import Forked

/// Observed-Remove Set. Can add and remove like a normal set.
/// Based on Convergent and commutative replicated data types by M Shapiro, N Preguiça, C Baquero, M Zawirski - 2011 - hal.inria.fr
public struct MergeableSet<T: Hashable> {
public struct MergeableSet<T: Hashable>: Hashable {

fileprivate struct Metadata {
fileprivate struct Metadata: Hashable {
var isDeleted: Bool
var timestamp: StableTimestamp

Expand Down Expand Up @@ -112,12 +112,6 @@ extension MergeableSet: Mergeable {
extension MergeableSet: Codable where T: Codable {}
extension MergeableSet.Metadata: Codable where T: Codable {}

extension MergeableSet: Equatable where T: Equatable {}
extension MergeableSet.Metadata: Equatable where T: Equatable {}

extension MergeableSet: Hashable where T: Hashable {}
extension MergeableSet.Metadata: Hashable where T: Hashable {}

extension MergeableSet: ExpressibleByArrayLiteral {
public init(arrayLiteral elements: T...) {
self = .init(array: elements)
Expand Down
7 changes: 2 additions & 5 deletions Sources/ForkedMerge/MergableTypes/MergeableValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import Forked
/// This allows the type to automatically merge simply by choosing the value that was written later.
/// Because there is a chance of timestamp collisions, a UUID is included to make collisions extremely unlikely.
/// Based on Convergent and commutative replicated data types by M Shapiro, N Preguiça, C Baquero, M Zawirski - 2011 - hal.inria.fr
public struct MergeableValue<T> {
public struct MergeableValue<T: Equatable>: Equatable {

fileprivate struct Entry: Identifiable {
fileprivate struct Entry: Identifiable, Equatable {
var value: T
var timestamp: TimeInterval
var id: UUID
Expand Down Expand Up @@ -51,8 +51,5 @@ extension MergeableValue: Mergeable {
extension MergeableValue: Codable where T: Codable {}
extension MergeableValue.Entry: Codable where T: Codable {}

extension MergeableValue: Equatable where T: Equatable {}
extension MergeableValue.Entry: Equatable where T: Equatable {}

extension MergeableValue: Hashable where T: Hashable {}
extension MergeableValue.Entry: Hashable where T: Hashable {}
15 changes: 1 addition & 14 deletions Sources/ForkedMerge/Mergers/Merger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Forked
/// simple most recent edit wins, to more advanced CRDT based approaches that use
/// diffing against a common ancestor.
public protocol Merger {
associatedtype T
associatedtype T: Equatable
init()
func merge(_ value: T, withSubordinate other: T, commonAncestor: T) throws -> T
}
Expand All @@ -17,19 +17,6 @@ public func merge<M: Merger>(withMergerType: M.Type, dominant: M.T, subordinate:
}

public func merge<M: Merger>(withMergerType: M.Type, dominant: M.T?, subordinate: M.T?, commonAncestor: M.T?) throws -> M.T? {
switch (dominant, subordinate, commonAncestor) {
case let (dominant?, subordinate?, commonAncestor?):
return try merge(withMergerType: M.self, dominant: dominant, subordinate: subordinate, commonAncestor: commonAncestor)
case (nil, nil, _):
return nil
case let (dominant?, _, _):
return dominant
case let (nil, subordinate?, _):
return subordinate
}
}

public func merge<M: Merger>(withMergerType: M.Type, dominant: M.T?, subordinate: M.T?, commonAncestor: M.T?) throws -> M.T? where M.T: Equatable {
switch (dominant, subordinate, commonAncestor) {
case let (dominant?, subordinate?, commonAncestor?):
return try merge(withMergerType: M.self, dominant: dominant, subordinate: subordinate, commonAncestor: commonAncestor)
Expand Down
2 changes: 1 addition & 1 deletion Sources/ForkedModel/Documentation.docc/ForkedModel.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,6 @@ When merging the dictionary, if the values for a given key are `Mergeable`, they

- All non-optional stored properties must have default values
- The `@ForkedModel` macro automatically makes your type conform to `Mergeable`
- Equatable properties without `@Merged` will use a "most recent wins" strategy, in a property-wise fashion
- Properties without `@Merged` will use a "most recent wins" strategy, in a property-wise fashion
- Non-equatable properties without `@Merged` will use a "most recent wins" strategy for the entire struct
- The merging strategy is determined at compile time and cannot be changed at runtime
9 changes: 0 additions & 9 deletions Sources/ForkedModel/EqualityFuncs.swift

This file was deleted.

2 changes: 1 addition & 1 deletion Sources/ForkedModelMacros/ForkedModelMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public struct ForkedModelMacro: ExtensionMacro {
let varName = varSyntax.bindings.first!.pattern.as(IdentifierPatternSyntax.self)!.identifier.text
let expr =
"""
if areEqualForForked(self.\(varName), commonAncestor.\(varName)) {
if self.\(varName) == commonAncestor.\(varName) {
merged.\(varName) = other.\(varName)
} else {
merged.\(varName) = self.\(varName)
Expand Down
4 changes: 2 additions & 2 deletions Tests/ForkedTests/ForkedModelMacros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -308,12 +308,12 @@ final class ForkedModelMacrosSuite: XCTestCase {
extension User: Forked.Mergeable {
public func merged(withSubordinate other: Self, commonAncestor: Self) throws -> Self {
var merged = self
if areEqualForForked(self.name, commonAncestor.name) {
if self.name == commonAncestor.name {
merged.name = other.name
} else {
merged.name = self.name
}
if areEqualForForked(self.age, commonAncestor.age) {
if self.age == commonAncestor.age {
merged.age = other.age
} else {
merged.age = self.age
Expand Down

0 comments on commit 38645de

Please sign in to comment.