Skip to content

Commit

Permalink
Merge pull request #136 from unsignedapps/flag-names
Browse files Browse the repository at this point in the history
Calculate flag display names at compile time instead of runtime
  • Loading branch information
bok- authored Dec 17, 2024
2 parents 8ed4724 + eef8078 commit 7ef7b5f
Show file tree
Hide file tree
Showing 10 changed files with 135 additions and 49 deletions.
7 changes: 3 additions & 4 deletions Sources/Vexil/Observability/FlagGroupWigwag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,8 @@ public struct FlagGroupWigwag<Output>: Sendable where Output: FlagContainer {
keyPath.key
}

/// An optional display name to give the flag. Only visible in flag editors like Vexillographer.
/// Default is to calculate one based on the property name.
public let name: String?
/// A human readable name for the flag group. Only visible in flag editors like Vexillographer.
public let name: String

/// A description of this flag. Only visible in flag editors like Vexillographer.
/// If this is nil the flag or flag group will be hidden.
Expand All @@ -56,7 +55,7 @@ public struct FlagGroupWigwag<Output>: Sendable where Output: FlagContainer {
/// Creates a Wigwag with the provided configuration.
public init(
keyPath: FlagKeyPath,
name: String?,
name: String,
description: String?,
displayOption: FlagGroupDisplayOption?,
lookup: any FlagLookup
Expand Down
7 changes: 3 additions & 4 deletions Sources/Vexil/Observability/FlagWigwag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,8 @@ public struct FlagWigwag<Output>: Sendable where Output: FlagValue {
/// The default value for this flag
public let defaultValue: Output

/// An optional display name to give the flag. Only visible in flag editors like Vexillographer.
/// Default is to calculate one based on the property name.
public let name: String?
/// A human readable name for the flag. Only visible in flag editors like Vexillographer.
public let name: String

/// A description of this flag. Only visible in flag editors like Vexillographer.
/// If this is nil the flag or flag group will be hidden.
Expand All @@ -59,7 +58,7 @@ public struct FlagWigwag<Output>: Sendable where Output: FlagValue {
/// Creates a Wigwag with the provided configuration.
public init(
keyPath: FlagKeyPath,
name: String?,
name: String,
defaultValue: Output,
description: String?,
displayOption: FlagDisplayOption,
Expand Down
4 changes: 2 additions & 2 deletions Sources/VexilMacros/FlagGroupMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public struct FlagGroupMacro {
wigwag: {
FlagGroupWigwag<\(type)>(
keyPath: \(key),
name: \(name ?? "nil"),
name: \(name ?? ExprSyntax(StringLiteralExprSyntax(content: propertyName.displayName))),
description: \(description ?? "nil"),
displayOption: \(displayOption ?? ".navigation"),
lookup: _flagLookup
Expand All @@ -97,7 +97,7 @@ public struct FlagGroupMacro {
"""
FlagGroupWigwag(
keyPath: \(key),
name: \(name ?? "nil"),
name: \(name ?? ExprSyntax(StringLiteralExprSyntax(content: propertyName.displayName))),
description: \(description ?? "nil"),
displayOption: \(displayOption ?? ".navigation"),
lookup: _flagLookup
Expand Down
2 changes: 1 addition & 1 deletion Sources/VexilMacros/FlagMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ public struct FlagMacro {
"""
FlagWigwag(
keyPath: \(key),
name: \(name ?? "nil"),
name: \(name ?? ExprSyntax(StringLiteralExprSyntax(content: propertyName.displayName))),
defaultValue: \(defaultValue),
description: \(description),
displayOption: \(display ?? ".default"),
Expand Down
88 changes: 88 additions & 0 deletions Sources/VexilMacros/Utilities/DisplayName.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Vexil open source project
//
// Copyright (c) 2024 Unsigned Apps and the open source contributors.
// Licensed under the MIT license
//
// See LICENSE for license information
//
// SPDX-License-Identifier: MIT
//
//===----------------------------------------------------------------------===//

import Foundation

extension String {
var displayName: String {
let uppercased = CharacterSet.uppercaseLetters
return (hasPrefix("_") ? String(dropFirst()) : self)
.separatedAtWordBoundaries
.map { CharacterSet(charactersIn: $0).isStrictSubset(of: uppercased) ? $0 : $0.capitalized }
.joined(separator: " ")
}

/// Separates a string at word boundaries, eg. `oneTwoThree` becomes `one Two Three`
///
/// Capital characters are determined by testing membership in `CharacterSet.uppercaseLetters`
/// and `CharacterSet.lowercaseLetters` (Unicode General Categories Lu and Lt).
/// The conversion to lower case uses `Locale.system`, also known as the ICU "root" locale. This means
/// the result is consistent regardless of the current user's locale and language preferences.
///
/// Adapted from JSONEncoder's `toSnakeCase()`
///
var separatedAtWordBoundaries: [String] {
guard !isEmpty else {
return []
}

let string = self

var words: [Range<String.Index>] = []
// The general idea of this algorithm is to split words on transition from lower to upper case, then on
// transition of >1 upper case characters to lowercase
//
// myProperty -> my_property
// myURLProperty -> my_url_property
//
// We assume, per Swift naming conventions, that the first character of the key is lowercase.
var wordStart = string.startIndex
var searchRange = string.index(after: wordStart) ..< string.endIndex

let uppercase = CharacterSet.uppercaseLetters.union(CharacterSet.decimalDigits)

// Find next uppercase character
while let upperCaseRange = string.rangeOfCharacter(from: uppercase, options: [], range: searchRange) {
let untilUpperCase = wordStart ..< upperCaseRange.lowerBound
words.append(untilUpperCase)

// Find next lowercase character
searchRange = upperCaseRange.lowerBound ..< searchRange.upperBound
guard let lowerCaseRange = string.rangeOfCharacter(from: CharacterSet.lowercaseLetters, options: [], range: searchRange) else {
// There are no more lower case letters. Just end here.
wordStart = searchRange.lowerBound
break
}

// Is the next lowercase letter more than 1 after the uppercase? If so, we encountered a group of uppercase
// letters that we should treat as its own word
let nextCharacterAfterCapital = string.index(after: upperCaseRange.lowerBound)
if lowerCaseRange.lowerBound == nextCharacterAfterCapital {
// The next character after capital is a lower case character and therefore not a word boundary.
// Continue searching for the next upper case for the boundary.
wordStart = upperCaseRange.lowerBound
} else {
// There was a range of >1 capital letters. Turn those into a word, stopping at the capital before the lower case character.
let beforeLowerIndex = string.index(before: lowerCaseRange.lowerBound)
words.append(upperCaseRange.lowerBound ..< beforeLowerIndex)

// Next word starts at the capital before the lowercase we just found
wordStart = beforeLowerIndex
}
searchRange = lowerCaseRange.upperBound ..< searchRange.upperBound
}
words.append(wordStart ..< searchRange.upperBound)

return words.map { string[$0].lowercased() }
}
}
12 changes: 6 additions & 6 deletions Tests/VexilMacroTests/EquatableFlagContainerMacroTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ final class EquatableFlagContainerMacroTests: XCTestCase {
var $someFlag: FlagWigwag<Bool> {
FlagWigwag(
keyPath: _flagKeyPath.append(.automatic("some-flag")),
name: nil,
name: "Some Flag",
defaultValue: false,
description: "Some Flag",
displayOption: .default,
Expand Down Expand Up @@ -180,7 +180,7 @@ final class EquatableFlagContainerMacroTests: XCTestCase {
var $someFlag: FlagWigwag<Bool> {
FlagWigwag(
keyPath: _flagKeyPath.append(.automatic("some-flag")),
name: nil,
name: "Some Flag",
defaultValue: false,
description: "Some Flag",
displayOption: .default,
Expand Down Expand Up @@ -257,7 +257,7 @@ final class EquatableFlagContainerMacroTests: XCTestCase {
var $someFlag: FlagWigwag<Bool> {
FlagWigwag(
keyPath: _flagKeyPath.append(.automatic("some-flag")),
name: nil,
name: "Some Flag",
defaultValue: false,
description: "Some Flag",
displayOption: .default,
Expand Down Expand Up @@ -332,7 +332,7 @@ final class EquatableFlagContainerMacroTests: XCTestCase {
var $someFlag: FlagWigwag<Bool> {
FlagWigwag(
keyPath: _flagKeyPath.append(.automatic("some-flag")),
name: nil,
name: "Some Flag",
defaultValue: false,
description: "Some Flag",
displayOption: .default,
Expand Down Expand Up @@ -436,7 +436,7 @@ final class EquatableFlagContainerMacroTests: XCTestCase {
wigwag: {
FlagGroupWigwag<GroupOfFlags>(
keyPath: _flagKeyPath.append(.automatic("flag-group")),
name: nil,
name: "Flag Group",
description: "Test Group",
displayOption: .navigation,
lookup: _flagLookup
Expand Down Expand Up @@ -530,7 +530,7 @@ final class EquatableFlagContainerMacroTests: XCTestCase {
wigwag: {
FlagGroupWigwag<GroupOfFlags>(
keyPath: _flagKeyPath.append(.automatic("flag-group")),
name: nil,
name: "Flag Group",
description: "Test Group",
displayOption: .navigation,
lookup: _flagLookup
Expand Down
2 changes: 1 addition & 1 deletion Tests/VexilMacroTests/FlagContainerMacroTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ final class FlagContainerMacroTests: XCTestCase {
wigwag: {
FlagGroupWigwag<GroupOfFlags>(
keyPath: _flagKeyPath.append(.automatic("flag-group")),
name: nil,
name: "Flag Group",
description: "Test Group",
displayOption: .navigation,
lookup: _flagLookup
Expand Down
24 changes: 12 additions & 12 deletions Tests/VexilMacroTests/FlagGroupMacroTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ final class FlagGroupMacroTests: XCTestCase {
var $testSubgroup: FlagGroupWigwag<SubgroupFlags> {
FlagGroupWigwag(
keyPath: _flagKeyPath.append(.automatic("test-subgroup")),
name: nil,
name: "Test Subgroup",
description: "Test Flag Group",
displayOption: .navigation,
lookup: _flagLookup
Expand Down Expand Up @@ -74,7 +74,7 @@ final class FlagGroupMacroTests: XCTestCase {
public var $testSubgroup: FlagGroupWigwag<SubgroupFlags> {
FlagGroupWigwag(
keyPath: _flagKeyPath.append(.automatic("test-subgroup")),
name: nil,
name: "Test Subgroup",
description: "Test Flag Group",
displayOption: .navigation,
lookup: _flagLookup
Expand Down Expand Up @@ -145,7 +145,7 @@ final class FlagGroupMacroTests: XCTestCase {
var $testSubgroup: FlagGroupWigwag<SubgroupFlags> {
FlagGroupWigwag(
keyPath: _flagKeyPath.append(.automatic("test-subgroup")),
name: nil,
name: "Test Subgroup",
description: "meow",
displayOption: .hidden,
lookup: _flagLookup
Expand Down Expand Up @@ -179,7 +179,7 @@ final class FlagGroupMacroTests: XCTestCase {
var $testSubgroup: FlagGroupWigwag<SubgroupFlags> {
FlagGroupWigwag(
keyPath: _flagKeyPath.append(.automatic("test-subgroup")),
name: nil,
name: "Test Subgroup",
description: "meow",
displayOption: .navigation,
lookup: _flagLookup
Expand Down Expand Up @@ -213,7 +213,7 @@ final class FlagGroupMacroTests: XCTestCase {
var $testSubgroup: FlagGroupWigwag<SubgroupFlags> {
FlagGroupWigwag(
keyPath: _flagKeyPath.append(.automatic("test-subgroup")),
name: nil,
name: "Test Subgroup",
description: "meow",
displayOption: .section,
lookup: _flagLookup
Expand Down Expand Up @@ -249,7 +249,7 @@ final class FlagGroupMacroTests: XCTestCase {
var $testSubgroup: FlagGroupWigwag<SubgroupFlags> {
FlagGroupWigwag(
keyPath: _flagKeyPath.append(.automatic("test-subgroup")),
name: nil,
name: "Test Subgroup",
description: "meow",
displayOption: .navigation,
lookup: _flagLookup
Expand Down Expand Up @@ -283,7 +283,7 @@ final class FlagGroupMacroTests: XCTestCase {
var $testSubgroup: FlagGroupWigwag<SubgroupFlags> {
FlagGroupWigwag(
keyPath: _flagKeyPath.append(.automatic("test-subgroup")),
name: nil,
name: "Test Subgroup",
description: "meow",
displayOption: .navigation,
lookup: _flagLookup
Expand Down Expand Up @@ -320,7 +320,7 @@ final class FlagGroupMacroTests: XCTestCase {
var $testSubgroup: FlagGroupWigwag<SubgroupFlags> {
FlagGroupWigwag(
keyPath: _flagKeyPath.append(.automatic("test-subgroup")),
name: nil,
name: "Test Subgroup",
description: "meow",
displayOption: .navigation,
lookup: _flagLookup
Expand Down Expand Up @@ -354,7 +354,7 @@ final class FlagGroupMacroTests: XCTestCase {
var $testSubgroup: FlagGroupWigwag<SubgroupFlags> {
FlagGroupWigwag(
keyPath: _flagKeyPath.append(.kebabcase("test-subgroup")),
name: nil,
name: "Test Subgroup",
description: "meow",
displayOption: .navigation,
lookup: _flagLookup
Expand Down Expand Up @@ -388,7 +388,7 @@ final class FlagGroupMacroTests: XCTestCase {
var $testSubgroup: FlagGroupWigwag<SubgroupFlags> {
FlagGroupWigwag(
keyPath: _flagKeyPath.append(.snakecase("test_subgroup")),
name: nil,
name: "Test Subgroup",
description: "meow",
displayOption: .navigation,
lookup: _flagLookup
Expand Down Expand Up @@ -422,7 +422,7 @@ final class FlagGroupMacroTests: XCTestCase {
var $testSubgroup: FlagGroupWigwag<SubgroupFlags> {
FlagGroupWigwag(
keyPath: _flagKeyPath,
name: nil,
name: "Test Subgroup",
description: "meow",
displayOption: .navigation,
lookup: _flagLookup
Expand Down Expand Up @@ -456,7 +456,7 @@ final class FlagGroupMacroTests: XCTestCase {
var $testSubgroup: FlagGroupWigwag<SubgroupFlags> {
FlagGroupWigwag(
keyPath: _flagKeyPath.append(.customKey("test")),
name: nil,
name: "Test Subgroup",
description: "meow",
displayOption: .navigation,
lookup: _flagLookup
Expand Down
Loading

0 comments on commit 7ef7b5f

Please sign in to comment.