Skip to content

Commit

Permalink
Merge pull request #106 from KeithBauerANZ/reduce-stored-properties
Browse files Browse the repository at this point in the history
Reduce code bloat from `FlagContainer`s
  • Loading branch information
bok- authored Dec 14, 2022
2 parents dc9ac0a + b097da9 commit 7165ddf
Show file tree
Hide file tree
Showing 3 changed files with 198 additions and 54 deletions.
11 changes: 0 additions & 11 deletions Sources/Vexil/Decorator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,6 @@ internal protocol Decorated {
func decorate (lookup: Lookup, label: String, codingPath: [String], config: VexilConfiguration)
}

/// An internal class that `Flag` and `FlagGroup`s store their information in.
/// It is specifically a class so that the `Flag` and `FlagGroup` structs can
/// mutate the `Decorator` while remaining immutable themselves.
///
internal class Decorator {
var key: String?
weak var lookup: Lookup?

init() {}
}

internal extension Sequence where Element == Mirror.Child {

typealias DecoratedChild = (label: String, value: Decorated)
Expand Down
139 changes: 112 additions & 27 deletions Sources/Vexil/Flag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,51 @@ public struct Flag<Value>: Decorated, Identifiable where Value: FlagValue {

// MARK: - Properties

// FlagContainers may have many flags, so to reduce code bloat
// it's important that each Flag have as few stored properties
// (with nontrivial copy behavior) as possible. We therefore use
// a single `Allocation` for all of Flag's stored properties.
var allocation: Allocation

/// All `Flag`s are `Identifiable`
public var id = UUID()
public var id: UUID {
get {
allocation.id
}
set {
if isKnownUniquelyReferenced(&allocation) == false {
allocation = allocation.copy()
}
allocation.id = newValue
}
}

/// A collection of information about this `Flag`, such as its display name and description.
public var info: FlagInfo
public var info: FlagInfo {
get {
allocation.info
}
set {
if isKnownUniquelyReferenced(&allocation) == false {
allocation = allocation.copy()
}
allocation.info = newValue
}
}

/// The default value for this `Flag` for when no sources are available, or if no
/// sources have a value specified for this flag.
public var defaultValue: Value
public var defaultValue: Value {
get {
allocation.defaultValue
}
set {
if isKnownUniquelyReferenced(&allocation) == false {
allocation = allocation.copy()
}
allocation.defaultValue = newValue
}
}

/// The `Flag` value. This is a calculated property based on the `FlagPole`s sources.
public var wrappedValue: Value {
Expand All @@ -48,7 +84,7 @@ public struct Flag<Value>: Decorated, Identifiable where Value: FlagValue {
/// The string-based Key for this `Flag`, as calculated during `init`. This key is
/// sent to the `FlagValueSource`s.
public var key: String {
return self.decorator.key!
return self.allocation.key!
}

/// A reference to the `Flag` itself is available as a projected value, in case you need
Expand Down Expand Up @@ -77,12 +113,12 @@ public struct Flag<Value>: Decorated, Identifiable where Value: FlagValue {
/// You can also specify `.hidden` to hide this flag from Vexillographer.
///
public init (name: String? = nil, codingKeyStrategy: CodingKeyStrategy = .default, default initialValue: Value, description: FlagInfo) {
self.codingKeyStrategy = codingKeyStrategy
self.defaultValue = initialValue

var info = description
info.name = name
self.info = info
self.init(
wrappedValue: initialValue,
name: name,
codingKeyStrategy: codingKeyStrategy,
description: description
)
}

/// Initialises a new `Flag` with the supplied info.
Expand All @@ -101,46 +137,49 @@ public struct Flag<Value>: Decorated, Identifiable where Value: FlagValue {
/// You can also specify `.hidden` to hide this flag from Vexillographer.
///
public init (wrappedValue: Value, name: String? = nil, codingKeyStrategy: CodingKeyStrategy = .default, description: FlagInfo) {
self.codingKeyStrategy = codingKeyStrategy
self.defaultValue = wrappedValue

var info = description
info.name = name
self.info = info
self.allocation = Allocation(
info: info,
defaultValue: wrappedValue,
codingKeyStrategy: codingKeyStrategy
)
}


// MARK: - Decorated Conformance

internal var decorator = Decorator()
internal let codingKeyStrategy: CodingKeyStrategy

/// Decorates the receiver with the given lookup info.
///
/// `self.key` is calculated during this step based on the supplied parameters. `lookup` is used by `self.wrappedValue`
/// to find out the current flag value from the source hierarchy.
///
internal func decorate (lookup: Lookup, label: String, codingPath: [String], config: VexilConfiguration) {
self.decorator.lookup = lookup

var action = self.codingKeyStrategy.codingKey(label: label)
internal func decorate (
lookup: Lookup,
label: String,
codingPath: [String],
config: VexilConfiguration
) {
self.allocation.lookup = lookup

var action = self.allocation.codingKeyStrategy.codingKey(label: label)
if action == .default {
action = config.codingPathStrategy.codingKey(label: label)
}

switch action {

case .append(let string):
self.decorator.key = (codingPath + [string])
self.allocation.key = (codingPath + [string])
.joined(separator: config.separator)

case .absolute(let string):
self.decorator.key = string
self.allocation.key = string

// these two options should really never happen, but just in case, use what we've got
case .default, .skip:
assertionFailure("Invalid `CodingKeyAction` found when attempting to create key name for Flag \(self)")
self.decorator.key = (codingPath + [label])
self.allocation.key = (codingPath + [label])
.joined(separator: config.separator)

}
Expand All @@ -150,7 +189,7 @@ public struct Flag<Value>: Decorated, Identifiable where Value: FlagValue {
// MARK: - Lookup Support

func value (in source: FlagValueSource?) -> LookupResult<Value>? {
guard let lookup = self.decorator.lookup, let key = self.decorator.key else {
guard let lookup = self.allocation.lookup, let key = self.allocation.key else {
return LookupResult(source: nil, value: self.defaultValue)
}
let value: LookupResult<Value>? = lookup.lookup(key: key, in: source)
Expand Down Expand Up @@ -192,6 +231,52 @@ extension Flag: CustomDebugStringConvertible {
}


// MARK: - Property Storage

extension Flag {

final class Allocation {
var id: UUID
var info: FlagInfo
var defaultValue: Value

// these are computed lazily during `decorate`
var key: String?
weak var lookup: Lookup?

var codingKeyStrategy: CodingKeyStrategy

init(
id: UUID = UUID(),
info: FlagInfo,
defaultValue: Value,
key: String? = nil,
lookup: Lookup? = nil,
codingKeyStrategy: CodingKeyStrategy
) {
self.id = id
self.info = info
self.defaultValue = defaultValue
self.key = key
self.lookup = lookup
self.codingKeyStrategy = codingKeyStrategy
}

func copy() -> Allocation {
Allocation(
id: id,
info: info,
defaultValue: defaultValue,
key: key,
lookup: lookup,
codingKeyStrategy: codingKeyStrategy
)
}
}

}


// MARK: - Real Time Flag Publishing

#if !os(Linux)
Expand All @@ -206,7 +291,7 @@ public extension Flag where Value: FlagValue & Equatable {
/// remove duplicates.
///
var publisher: AnyPublisher<Value, Never> {
decorator.lookup!.publisher(key: self.key)
allocation.lookup!.publisher(key: self.key)
.removeDuplicates()
.eraseToAnyPublisher()
}
Expand All @@ -224,7 +309,7 @@ public extension Flag {
/// remove duplicates.
///
var publisher: AnyPublisher<Value, Never> {
decorator.lookup!.publisher(key: self.key)
allocation.lookup!.publisher(key: self.key)
}

}
Expand Down
102 changes: 86 additions & 16 deletions Sources/Vexil/Group.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,39 @@ import Foundation
@propertyWrapper
public struct FlagGroup<Group>: Decorated, Identifiable where Group: FlagContainer {

// FlagContainers may have many flag groups, so to reduce code bloat
// it's important that each FlagGroup have as few stored properties
// (with nontrivial copy behavior) as possible. We therefore use
// a single `Allocation` for all of FlagGroup's stored properties.
var allocation: Allocation

/// All `FlagGroup`s are `Identifiable`
public let id = UUID()
public var id: UUID {
allocation.id
}

/// A collection of information about this `FlagGroup` such as its display name and description.
public let info: FlagInfo
public var info: FlagInfo {
allocation.info
}

/// The `FlagContainer` being wrapped.
public var wrappedValue: Group
public var wrappedValue: Group {
get {
allocation.wrappedValue
}
set {
if isKnownUniquelyReferenced(&allocation) == false {
allocation = allocation.copy()
}
allocation.wrappedValue = newValue
}
}

/// How we should display this group in Vexillographer
public let display: Display
public var display: Display {
allocation.display
}


// MARK: - Initialisation
Expand All @@ -48,28 +70,26 @@ public struct FlagGroup<Group>: Decorated, Identifiable where Group: FlagContain
/// - display: Whether we should display this FlagGroup as using a `NavigationLink` or as a `Section` in Vexillographer
///
public init (name: String? = nil, codingKeyStrategy: CodingKeyStrategy = .default, description: FlagInfo, display: Display = .navigation) {
self.codingKeyStrategy = codingKeyStrategy
self.wrappedValue = Group()
self.display = display

var info = description
info.name = name
self.info = info
self.allocation = Allocation(
info: info,
wrappedValue: Group(),
display: display,
codingKeyStrategy: codingKeyStrategy
)
}


// MARK: - Decoratod Conformance

internal var decorator = Decorator()
private let codingKeyStrategy: CodingKeyStrategy
// MARK: - Decorated Conformance

/// Decorates the receiver with the given lookup info.
///
/// The `key` for this part of the flag tree is calculated during this step based on the supplied parameters. All info is passed through to
/// any `Flag` or `FlagGroup` contained within the receiver.
///
func decorate(lookup: Lookup, label: String, codingPath: [String], config: VexilConfiguration) {
var action = self.codingKeyStrategy.codingKey(label: label)
var action = self.allocation.codingKeyStrategy.codingKey(label: label)
if action == .default {
action = config.codingPathStrategy.codingKey(label: label)
}
Expand All @@ -89,8 +109,9 @@ public struct FlagGroup<Group>: Decorated, Identifiable where Group: FlagContain

}

self.decorator.key = codingPath.joined(separator: config.separator)
self.decorator.lookup = lookup
// FIXME: for compatibility with existing behavior, this doesn't use `isKnownUniquelyReferenced`, but perhaps it should?
self.allocation.key = codingPath.joined(separator: config.separator)
self.allocation.lookup = lookup

Mirror(reflecting: self.wrappedValue)
.children
Expand Down Expand Up @@ -135,6 +156,55 @@ extension FlagGroup: CustomDebugStringConvertible {
}


// MARK: - Property Storage

extension FlagGroup {

final class Allocation {
let id: UUID
let info: FlagInfo
var wrappedValue: Group
let display: Display

// these are computed lazily during `decorate`
var key: String?
weak var lookup: Lookup?

let codingKeyStrategy: CodingKeyStrategy

init(
id: UUID = UUID(),
info: FlagInfo,
wrappedValue: Group,
display: Display,
key: String? = nil,
lookup: Lookup? = nil,
codingKeyStrategy: CodingKeyStrategy
) {
self.id = id
self.info = info
self.wrappedValue = wrappedValue
self.display = display
self.key = key
self.lookup = lookup
self.codingKeyStrategy = codingKeyStrategy
}

func copy() -> Allocation {
Allocation(
info: info,
wrappedValue: wrappedValue,
display: display,
key: key,
lookup: lookup,
codingKeyStrategy: codingKeyStrategy
)
}
}

}


// MARK: - Group Display

public extension FlagGroup {
Expand Down

0 comments on commit 7165ddf

Please sign in to comment.