Skip to content

Commit d55ebea

Browse files
committed
Provide FlagValueSource's with a way to create FlagKeyPaths that are consistent with the FlagPole.
1 parent 21bd870 commit d55ebea

18 files changed

+172
-61
lines changed

Sources/Vexil/Configuration.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ public struct VexilConfiguration: Sendable {
4949
public static var `default`: VexilConfiguration {
5050
VexilConfiguration()
5151
}
52+
53+
func makeKeyPathMapper() -> @Sendable (String) -> FlagKeyPath {
54+
{
55+
FlagKeyPath($0, separator: separator, strategy: codingPathStrategy)
56+
}
57+
}
5258
}
5359

5460

Sources/Vexil/KeyPath.swift

Lines changed: 76 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
public struct FlagKeyPath: Hashable, Sendable {
1515

16-
public enum Key: Hashable, Sendable {
16+
public enum Key: Sendable {
1717
case root
1818
case automatic(String)
1919
case kebabcase(String)
@@ -25,11 +25,26 @@ public struct FlagKeyPath: Hashable, Sendable {
2525
// MARK: - Properties
2626

2727
let keyPath: [Key]
28+
29+
public let key: String
2830
public let separator: String
2931
public let strategy: VexilConfiguration.CodingKeyStrategy
3032

3133
// MARK: - Initialisation
3234

35+
/// Memberwise initialiser
36+
init(
37+
_ keyPath: [Key],
38+
separator: String = ".",
39+
strategy: VexilConfiguration.CodingKeyStrategy = .default,
40+
key: String
41+
) {
42+
self.keyPath = keyPath
43+
self.separator = separator
44+
self.strategy = strategy
45+
self.key = key
46+
}
47+
3348
public init(
3449
_ keyPath: [Key],
3550
separator: String = ".",
@@ -38,6 +53,18 @@ public struct FlagKeyPath: Hashable, Sendable {
3853
self.keyPath = keyPath
3954
self.separator = separator
4055
self.strategy = strategy
56+
57+
self.key = {
58+
var toReturn = [String]()
59+
for path in keyPath {
60+
switch path.stringKeyMode(strategy: strategy) {
61+
case let .append(key): toReturn.append(key)
62+
case let .replace(key): return key
63+
case .root: break
64+
}
65+
}
66+
return toReturn.joined(separator: separator)
67+
}()
4168
}
4269

4370
public init(_ key: String, separator: String = ".", strategy: VexilConfiguration.CodingKeyStrategy = .default) {
@@ -50,27 +77,22 @@ public struct FlagKeyPath: Hashable, Sendable {
5077
FlagKeyPath(
5178
keyPath + [ key ],
5279
separator: separator,
53-
strategy: strategy
80+
strategy: strategy,
81+
key: {
82+
switch key.stringKeyMode(strategy: strategy) {
83+
case let .append(string) where self.key.isEmpty:
84+
string
85+
case let .append(string):
86+
self.key + separator + string
87+
case let .replace(string):
88+
string
89+
case .root:
90+
self.key // mostly a noop
91+
}
92+
}()
5493
)
5594
}
5695

57-
public var key: String {
58-
var toReturn = [String]()
59-
for path in keyPath {
60-
switch (path, strategy) {
61-
case let (.automatic(key), .default), let (.automatic(key), .kebabcase), let (.kebabcase(key), _), let (.customKey(key), _):
62-
toReturn.append(key)
63-
case let (.automatic(key), .snakecase), let (.snakecase(key), _):
64-
toReturn.append(key.replacingOccurrences(of: "-", with: "_"))
65-
case let (.customKeyPath(key), _):
66-
return key
67-
case (.root, _):
68-
break
69-
}
70-
}
71-
return toReturn.joined(separator: separator)
72-
}
73-
7496
static func root(separator: String, strategy: VexilConfiguration.CodingKeyStrategy) -> FlagKeyPath {
7597
FlagKeyPath(
7698
[ .root ],
@@ -79,4 +101,39 @@ public struct FlagKeyPath: Hashable, Sendable {
79101
)
80102
}
81103

104+
// MARK: - Hashable
105+
106+
// Equality for us is based on the output key, not how it was created. Otherwise
107+
// keys coming back from external sources will never match an internally created one.
108+
109+
public static func == (lhs: FlagKeyPath, rhs: FlagKeyPath) -> Bool {
110+
lhs.key == rhs.key
111+
}
112+
113+
public func hash(into hasher: inout Hasher) {
114+
hasher.combine(key)
115+
}
116+
117+
}
118+
119+
120+
private extension FlagKeyPath.Key {
121+
enum Mode {
122+
case append(String)
123+
case replace(String)
124+
case root
125+
}
126+
127+
func stringKeyMode(strategy: VexilConfiguration.CodingKeyStrategy) -> Mode {
128+
switch (self, strategy) {
129+
case let (.automatic(key), .default), let (.automatic(key), .kebabcase), let (.kebabcase(key), _), let (.customKey(key), _):
130+
.append(key)
131+
case let (.automatic(key), .snakecase), let (.snakecase(key), _):
132+
.append(key.replacingOccurrences(of: "-", with: "_"))
133+
case let (.customKeyPath(key), _):
134+
.replace(key)
135+
case (.root, _):
136+
.root
137+
}
138+
}
82139
}

Sources/Vexil/Pole.swift

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -418,14 +418,34 @@ public final class FlagPole<RootGroup>: Sendable where RootGroup: FlagContainer
418418

419419
extension FlagPole: CustomDebugStringConvertible {
420420
public var debugDescription: String {
421-
"FlagPole<\(String(describing: RootGroup.self))>("
422-
+ Mirror(reflecting: rootGroup).children
423-
.map { _, value -> String in
424-
(value as? CustomDebugStringConvertible)?.debugDescription
425-
?? (value as? CustomStringConvertible)?.description
426-
?? String(describing: value)
427-
}
428-
.joined(separator: "; ")
429-
+ ")"
421+
let visitor = DebugDescriptionVisitor()
422+
walk(visitor: visitor)
423+
return "FlagPole<\(String(describing: RootGroup.self))>(\(visitor.valueDescriptions.joined(separator: "; ")))"
430424
}
431425
}
426+
427+
private final class DebugDescriptionVisitor: FlagVisitor {
428+
429+
var valueDescriptions = [String]()
430+
431+
func visitFlag<Value>(
432+
keyPath: FlagKeyPath,
433+
value: () -> Value?,
434+
defaultValue: Value,
435+
wigwag: () -> FlagWigwag<Value>
436+
) where Value: FlagValue {
437+
guard let value = value() else {
438+
valueDescriptions.append("\(keyPath.key)=nil")
439+
return
440+
}
441+
442+
if let debug = value as? CustomDebugStringConvertible {
443+
valueDescriptions.append("\(keyPath.key)=\(debug)")
444+
} else if let description = value as? CustomStringConvertible {
445+
valueDescriptions.append("\(keyPath.key)=\(description)")
446+
} else {
447+
valueDescriptions.append("\(keyPath.key)=\(value)")
448+
}
449+
}
450+
451+
}

Sources/Vexil/Snapshots/MutableFlagContainer.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ public class MutableFlagContainer<Container> where Container: FlagContainer {
6161
}
6262
}
6363

64+
/// A @dynamicMemberLookup implementation for all other properties (eg extensions). This is get-only.
65+
@_disfavoredOverload
66+
public subscript<Value>(dynamicMember dynamicMember: KeyPath<Container, Value>) -> Value {
67+
container[keyPath: dynamicMember]
68+
}
69+
6470
/// Internal initialiser used to create MutableFlagGroups for a given subgroup and snapshot
6571
init(group: Container, source: any FlagValueSource) {
6672
self.container = group

Sources/Vexil/Snapshots/Snapshot+FlagValueSource.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ extension Snapshot: FlagValueSource {
2727
set(value, key: key)
2828
}
2929

30-
public var flagValueChanges: FlagChangeStream {
30+
public func flagValueChanges(keyPathMapper: (String) -> FlagKeyPath) -> FlagChangeStream {
3131
stream.stream
3232
}
3333

Sources/Vexil/Snapshots/Snapshot.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public final class Snapshot<RootGroup>: Sendable where RootGroup: FlagContainer
8383
RootGroup(_flagKeyPath: rootKeyPath, _flagLookup: self)
8484
}
8585

86-
let stream = StreamManager.Stream()
86+
let stream: StreamManager.Stream
8787

8888

8989
// MARK: - Initialisation
@@ -97,6 +97,7 @@ public final class Snapshot<RootGroup>: Sendable where RootGroup: FlagContainer
9797
self.rootKeyPath = flagPole.rootKeyPath
9898
self.values = .init(initialState: [:])
9999
self.displayName = displayName
100+
self.stream = StreamManager.Stream(keyPathMapper: flagPole._configuration.makeKeyPathMapper())
100101

101102
if let source {
102103
populateValuesFrom(source, flagPole: flagPole, keys: keys)
@@ -107,6 +108,7 @@ public final class Snapshot<RootGroup>: Sendable where RootGroup: FlagContainer
107108
self.rootKeyPath = flagPole.rootKeyPath
108109
self.values = .init(initialState: [:])
109110
self.displayName = displayName
111+
self.stream = StreamManager.Stream(keyPathMapper: flagPole._configuration.makeKeyPathMapper())
110112

111113
if let source {
112114
switch change {
@@ -122,6 +124,7 @@ public final class Snapshot<RootGroup>: Sendable where RootGroup: FlagContainer
122124
self.rootKeyPath = flagPole.rootKeyPath
123125
self.values = snapshot.values
124126
self.displayName = displayName
127+
self.stream = StreamManager.Stream(keyPathMapper: flagPole._configuration.makeKeyPathMapper())
125128
}
126129

127130

Sources/Vexil/Sources/FlagValueDictionary+FlagValueSource.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,13 @@ extension FlagValueDictionary: FlagValueSource {
3434
storage.removeValue(forKey: key)
3535
}
3636
}
37-
stream.send(.some([ FlagKeyPath(key) ]))
37+
continuation.yield(key)
3838
}
3939

40-
public var flagValueChanges: FlagChangeStream {
41-
stream.stream
40+
public func flagValueChanges(keyPathMapper: @Sendable @escaping (String) -> FlagKeyPath) -> AsyncMapSequence<AsyncStream<String>, FlagChange> {
41+
stream.map {
42+
FlagChange.some([ keyPathMapper($0) ])
43+
}
4244
}
4345

4446
}

Sources/Vexil/Sources/FlagValueDictionary.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ public final class FlagValueDictionary: Identifiable, ExpressibleByDictionaryLit
3737

3838
let storage: Lock<DictionaryType>
3939

40-
let stream = StreamManager.Stream()
40+
let stream: AsyncStream<String>
41+
let continuation: AsyncStream<String>.Continuation
4142

4243

4344
// MARK: - Initialisation
@@ -46,12 +47,14 @@ public final class FlagValueDictionary: Identifiable, ExpressibleByDictionaryLit
4647
init(id: String, storage: DictionaryType) {
4748
self.id = id
4849
self.storage = .init(initialState: storage)
50+
(self.stream, self.continuation) = AsyncStream.makeStream()
4951
}
5052

5153
/// Initialises an empty `FlagValueDictionary`
5254
public init() {
5355
self.id = UUID().uuidString
5456
self.storage = .init(initialState: [:])
57+
(self.stream, self.continuation) = AsyncStream.makeStream()
5558
}
5659

5760
/// Initialises a `FlagValueDictionary` with the specified dictionary
@@ -60,6 +63,7 @@ public final class FlagValueDictionary: Identifiable, ExpressibleByDictionaryLit
6063
self.storage = .init(initialState: sequence.reduce(into: [:]) { dict, pair in
6164
dict.updateValue(pair.value, forKey: pair.key)
6265
})
66+
(self.stream, self.continuation) = AsyncStream.makeStream()
6367
}
6468

6569
/// Initialises a `FlagValueDictionary` using a dictionary literal
@@ -68,6 +72,7 @@ public final class FlagValueDictionary: Identifiable, ExpressibleByDictionaryLit
6872
self.storage = .init(initialState: elements.reduce(into: [:]) { dict, pair in
6973
dict.updateValue(pair.1, forKey: pair.0)
7074
})
75+
(self.stream, self.continuation) = AsyncStream.makeStream()
7176
}
7277

7378
// MARK: - Dictionary Access
@@ -88,6 +93,7 @@ public final class FlagValueDictionary: Identifiable, ExpressibleByDictionaryLit
8893
let container = try decoder.container(keyedBy: CodingKeys.self)
8994
self.id = try container.decode(String.self, forKey: .id)
9095
self.storage = try .init(initialState: container.decode(DictionaryType.self, forKey: .storage))
96+
(self.stream, self.continuation) = AsyncStream.makeStream()
9197
}
9298

9399
public func encode(to encoder: any Encoder) throws {

Sources/Vexil/Sources/FlagValueSource.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ public protocol FlagValueSource: AnyObject & Sendable {
4444

4545
/// Return an `AsyncSequence` that emits ``FlagChange`` values any time flag values have changed.
4646
/// If your implementation does not support real-time flag value monitoring you can return an ``EmptyFlagChangeStream``.
47-
var flagValueChanges: ChangeStream { get }
47+
///
48+
/// This method is called with an optional closure you can use to convert String-based key paths
49+
/// back into FlagKeyPaths according to the configuration of the receiving FlagPole.
50+
///
51+
func flagValueChanges(keyPathMapper: @Sendable @escaping (String) -> FlagKeyPath) -> ChangeStream
4852

4953
}
5054

Sources/Vexil/Sources/FlagValueSourceCoordinator.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,9 @@ extension FlagValueSourceCoordinator: FlagValueSource {
7272
}
7373
}
7474

75-
public var flagValueChanges: Source.ChangeStream {
75+
public func flagValueChanges(keyPathMapper: @Sendable @escaping (String) -> FlagKeyPath) -> Source.ChangeStream {
7676
source.withLockUnchecked {
77-
$0.flagValueChanges
77+
$0.flagValueChanges(keyPathMapper: keyPathMapper)
7878
}
7979
}
8080

0 commit comments

Comments
 (0)