Skip to content

Commit 7ef7b5f

Browse files
authored
Merge pull request #136 from unsignedapps/flag-names
Calculate flag display names at compile time instead of runtime
2 parents 8ed4724 + eef8078 commit 7ef7b5f

File tree

10 files changed

+135
-49
lines changed

10 files changed

+135
-49
lines changed

Sources/Vexil/Observability/FlagGroupWigwag.swift

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,8 @@ public struct FlagGroupWigwag<Output>: Sendable where Output: FlagContainer {
3636
keyPath.key
3737
}
3838

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

4342
/// A description of this flag. Only visible in flag editors like Vexillographer.
4443
/// If this is nil the flag or flag group will be hidden.
@@ -56,7 +55,7 @@ public struct FlagGroupWigwag<Output>: Sendable where Output: FlagContainer {
5655
/// Creates a Wigwag with the provided configuration.
5756
public init(
5857
keyPath: FlagKeyPath,
59-
name: String?,
58+
name: String,
6059
description: String?,
6160
displayOption: FlagGroupDisplayOption?,
6261
lookup: any FlagLookup

Sources/Vexil/Observability/FlagWigwag.swift

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,8 @@ public struct FlagWigwag<Output>: Sendable where Output: FlagValue {
3939
/// The default value for this flag
4040
public let defaultValue: Output
4141

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

4645
/// A description of this flag. Only visible in flag editors like Vexillographer.
4746
/// If this is nil the flag or flag group will be hidden.
@@ -59,7 +58,7 @@ public struct FlagWigwag<Output>: Sendable where Output: FlagValue {
5958
/// Creates a Wigwag with the provided configuration.
6059
public init(
6160
keyPath: FlagKeyPath,
62-
name: String?,
61+
name: String,
6362
defaultValue: Output,
6463
description: String?,
6564
displayOption: FlagDisplayOption,

Sources/VexilMacros/FlagGroupMacro.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public struct FlagGroupMacro {
7979
wigwag: {
8080
FlagGroupWigwag<\(type)>(
8181
keyPath: \(key),
82-
name: \(name ?? "nil"),
82+
name: \(name ?? ExprSyntax(StringLiteralExprSyntax(content: propertyName.displayName))),
8383
description: \(description ?? "nil"),
8484
displayOption: \(displayOption ?? ".navigation"),
8585
lookup: _flagLookup
@@ -97,7 +97,7 @@ public struct FlagGroupMacro {
9797
"""
9898
FlagGroupWigwag(
9999
keyPath: \(key),
100-
name: \(name ?? "nil"),
100+
name: \(name ?? ExprSyntax(StringLiteralExprSyntax(content: propertyName.displayName))),
101101
description: \(description ?? "nil"),
102102
displayOption: \(displayOption ?? ".navigation"),
103103
lookup: _flagLookup

Sources/VexilMacros/FlagMacro.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ public struct FlagMacro {
107107
"""
108108
FlagWigwag(
109109
keyPath: \(key),
110-
name: \(name ?? "nil"),
110+
name: \(name ?? ExprSyntax(StringLiteralExprSyntax(content: propertyName.displayName))),
111111
defaultValue: \(defaultValue),
112112
description: \(description),
113113
displayOption: \(display ?? ".default"),
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Vexil open source project
4+
//
5+
// Copyright (c) 2024 Unsigned Apps and the open source contributors.
6+
// Licensed under the MIT license
7+
//
8+
// See LICENSE for license information
9+
//
10+
// SPDX-License-Identifier: MIT
11+
//
12+
//===----------------------------------------------------------------------===//
13+
14+
import Foundation
15+
16+
extension String {
17+
var displayName: String {
18+
let uppercased = CharacterSet.uppercaseLetters
19+
return (hasPrefix("_") ? String(dropFirst()) : self)
20+
.separatedAtWordBoundaries
21+
.map { CharacterSet(charactersIn: $0).isStrictSubset(of: uppercased) ? $0 : $0.capitalized }
22+
.joined(separator: " ")
23+
}
24+
25+
/// Separates a string at word boundaries, eg. `oneTwoThree` becomes `one Two Three`
26+
///
27+
/// Capital characters are determined by testing membership in `CharacterSet.uppercaseLetters`
28+
/// and `CharacterSet.lowercaseLetters` (Unicode General Categories Lu and Lt).
29+
/// The conversion to lower case uses `Locale.system`, also known as the ICU "root" locale. This means
30+
/// the result is consistent regardless of the current user's locale and language preferences.
31+
///
32+
/// Adapted from JSONEncoder's `toSnakeCase()`
33+
///
34+
var separatedAtWordBoundaries: [String] {
35+
guard !isEmpty else {
36+
return []
37+
}
38+
39+
let string = self
40+
41+
var words: [Range<String.Index>] = []
42+
// The general idea of this algorithm is to split words on transition from lower to upper case, then on
43+
// transition of >1 upper case characters to lowercase
44+
//
45+
// myProperty -> my_property
46+
// myURLProperty -> my_url_property
47+
//
48+
// We assume, per Swift naming conventions, that the first character of the key is lowercase.
49+
var wordStart = string.startIndex
50+
var searchRange = string.index(after: wordStart) ..< string.endIndex
51+
52+
let uppercase = CharacterSet.uppercaseLetters.union(CharacterSet.decimalDigits)
53+
54+
// Find next uppercase character
55+
while let upperCaseRange = string.rangeOfCharacter(from: uppercase, options: [], range: searchRange) {
56+
let untilUpperCase = wordStart ..< upperCaseRange.lowerBound
57+
words.append(untilUpperCase)
58+
59+
// Find next lowercase character
60+
searchRange = upperCaseRange.lowerBound ..< searchRange.upperBound
61+
guard let lowerCaseRange = string.rangeOfCharacter(from: CharacterSet.lowercaseLetters, options: [], range: searchRange) else {
62+
// There are no more lower case letters. Just end here.
63+
wordStart = searchRange.lowerBound
64+
break
65+
}
66+
67+
// Is the next lowercase letter more than 1 after the uppercase? If so, we encountered a group of uppercase
68+
// letters that we should treat as its own word
69+
let nextCharacterAfterCapital = string.index(after: upperCaseRange.lowerBound)
70+
if lowerCaseRange.lowerBound == nextCharacterAfterCapital {
71+
// The next character after capital is a lower case character and therefore not a word boundary.
72+
// Continue searching for the next upper case for the boundary.
73+
wordStart = upperCaseRange.lowerBound
74+
} else {
75+
// There was a range of >1 capital letters. Turn those into a word, stopping at the capital before the lower case character.
76+
let beforeLowerIndex = string.index(before: lowerCaseRange.lowerBound)
77+
words.append(upperCaseRange.lowerBound ..< beforeLowerIndex)
78+
79+
// Next word starts at the capital before the lowercase we just found
80+
wordStart = beforeLowerIndex
81+
}
82+
searchRange = lowerCaseRange.upperBound ..< searchRange.upperBound
83+
}
84+
words.append(wordStart ..< searchRange.upperBound)
85+
86+
return words.map { string[$0].lowercased() }
87+
}
88+
}

Tests/VexilMacroTests/EquatableFlagContainerMacroTests.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ final class EquatableFlagContainerMacroTests: XCTestCase {
9191
var $someFlag: FlagWigwag<Bool> {
9292
FlagWigwag(
9393
keyPath: _flagKeyPath.append(.automatic("some-flag")),
94-
name: nil,
94+
name: "Some Flag",
9595
defaultValue: false,
9696
description: "Some Flag",
9797
displayOption: .default,
@@ -180,7 +180,7 @@ final class EquatableFlagContainerMacroTests: XCTestCase {
180180
var $someFlag: FlagWigwag<Bool> {
181181
FlagWigwag(
182182
keyPath: _flagKeyPath.append(.automatic("some-flag")),
183-
name: nil,
183+
name: "Some Flag",
184184
defaultValue: false,
185185
description: "Some Flag",
186186
displayOption: .default,
@@ -257,7 +257,7 @@ final class EquatableFlagContainerMacroTests: XCTestCase {
257257
var $someFlag: FlagWigwag<Bool> {
258258
FlagWigwag(
259259
keyPath: _flagKeyPath.append(.automatic("some-flag")),
260-
name: nil,
260+
name: "Some Flag",
261261
defaultValue: false,
262262
description: "Some Flag",
263263
displayOption: .default,
@@ -332,7 +332,7 @@ final class EquatableFlagContainerMacroTests: XCTestCase {
332332
var $someFlag: FlagWigwag<Bool> {
333333
FlagWigwag(
334334
keyPath: _flagKeyPath.append(.automatic("some-flag")),
335-
name: nil,
335+
name: "Some Flag",
336336
defaultValue: false,
337337
description: "Some Flag",
338338
displayOption: .default,
@@ -436,7 +436,7 @@ final class EquatableFlagContainerMacroTests: XCTestCase {
436436
wigwag: {
437437
FlagGroupWigwag<GroupOfFlags>(
438438
keyPath: _flagKeyPath.append(.automatic("flag-group")),
439-
name: nil,
439+
name: "Flag Group",
440440
description: "Test Group",
441441
displayOption: .navigation,
442442
lookup: _flagLookup
@@ -530,7 +530,7 @@ final class EquatableFlagContainerMacroTests: XCTestCase {
530530
wigwag: {
531531
FlagGroupWigwag<GroupOfFlags>(
532532
keyPath: _flagKeyPath.append(.automatic("flag-group")),
533-
name: nil,
533+
name: "Flag Group",
534534
description: "Test Group",
535535
displayOption: .navigation,
536536
lookup: _flagLookup

Tests/VexilMacroTests/FlagContainerMacroTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ final class FlagContainerMacroTests: XCTestCase {
184184
wigwag: {
185185
FlagGroupWigwag<GroupOfFlags>(
186186
keyPath: _flagKeyPath.append(.automatic("flag-group")),
187-
name: nil,
187+
name: "Flag Group",
188188
description: "Test Group",
189189
displayOption: .navigation,
190190
lookup: _flagLookup

Tests/VexilMacroTests/FlagGroupMacroTests.swift

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ final class FlagGroupMacroTests: XCTestCase {
4040
var $testSubgroup: FlagGroupWigwag<SubgroupFlags> {
4141
FlagGroupWigwag(
4242
keyPath: _flagKeyPath.append(.automatic("test-subgroup")),
43-
name: nil,
43+
name: "Test Subgroup",
4444
description: "Test Flag Group",
4545
displayOption: .navigation,
4646
lookup: _flagLookup
@@ -74,7 +74,7 @@ final class FlagGroupMacroTests: XCTestCase {
7474
public var $testSubgroup: FlagGroupWigwag<SubgroupFlags> {
7575
FlagGroupWigwag(
7676
keyPath: _flagKeyPath.append(.automatic("test-subgroup")),
77-
name: nil,
77+
name: "Test Subgroup",
7878
description: "Test Flag Group",
7979
displayOption: .navigation,
8080
lookup: _flagLookup
@@ -145,7 +145,7 @@ final class FlagGroupMacroTests: XCTestCase {
145145
var $testSubgroup: FlagGroupWigwag<SubgroupFlags> {
146146
FlagGroupWigwag(
147147
keyPath: _flagKeyPath.append(.automatic("test-subgroup")),
148-
name: nil,
148+
name: "Test Subgroup",
149149
description: "meow",
150150
displayOption: .hidden,
151151
lookup: _flagLookup
@@ -179,7 +179,7 @@ final class FlagGroupMacroTests: XCTestCase {
179179
var $testSubgroup: FlagGroupWigwag<SubgroupFlags> {
180180
FlagGroupWigwag(
181181
keyPath: _flagKeyPath.append(.automatic("test-subgroup")),
182-
name: nil,
182+
name: "Test Subgroup",
183183
description: "meow",
184184
displayOption: .navigation,
185185
lookup: _flagLookup
@@ -213,7 +213,7 @@ final class FlagGroupMacroTests: XCTestCase {
213213
var $testSubgroup: FlagGroupWigwag<SubgroupFlags> {
214214
FlagGroupWigwag(
215215
keyPath: _flagKeyPath.append(.automatic("test-subgroup")),
216-
name: nil,
216+
name: "Test Subgroup",
217217
description: "meow",
218218
displayOption: .section,
219219
lookup: _flagLookup
@@ -249,7 +249,7 @@ final class FlagGroupMacroTests: XCTestCase {
249249
var $testSubgroup: FlagGroupWigwag<SubgroupFlags> {
250250
FlagGroupWigwag(
251251
keyPath: _flagKeyPath.append(.automatic("test-subgroup")),
252-
name: nil,
252+
name: "Test Subgroup",
253253
description: "meow",
254254
displayOption: .navigation,
255255
lookup: _flagLookup
@@ -283,7 +283,7 @@ final class FlagGroupMacroTests: XCTestCase {
283283
var $testSubgroup: FlagGroupWigwag<SubgroupFlags> {
284284
FlagGroupWigwag(
285285
keyPath: _flagKeyPath.append(.automatic("test-subgroup")),
286-
name: nil,
286+
name: "Test Subgroup",
287287
description: "meow",
288288
displayOption: .navigation,
289289
lookup: _flagLookup
@@ -320,7 +320,7 @@ final class FlagGroupMacroTests: XCTestCase {
320320
var $testSubgroup: FlagGroupWigwag<SubgroupFlags> {
321321
FlagGroupWigwag(
322322
keyPath: _flagKeyPath.append(.automatic("test-subgroup")),
323-
name: nil,
323+
name: "Test Subgroup",
324324
description: "meow",
325325
displayOption: .navigation,
326326
lookup: _flagLookup
@@ -354,7 +354,7 @@ final class FlagGroupMacroTests: XCTestCase {
354354
var $testSubgroup: FlagGroupWigwag<SubgroupFlags> {
355355
FlagGroupWigwag(
356356
keyPath: _flagKeyPath.append(.kebabcase("test-subgroup")),
357-
name: nil,
357+
name: "Test Subgroup",
358358
description: "meow",
359359
displayOption: .navigation,
360360
lookup: _flagLookup
@@ -388,7 +388,7 @@ final class FlagGroupMacroTests: XCTestCase {
388388
var $testSubgroup: FlagGroupWigwag<SubgroupFlags> {
389389
FlagGroupWigwag(
390390
keyPath: _flagKeyPath.append(.snakecase("test_subgroup")),
391-
name: nil,
391+
name: "Test Subgroup",
392392
description: "meow",
393393
displayOption: .navigation,
394394
lookup: _flagLookup
@@ -422,7 +422,7 @@ final class FlagGroupMacroTests: XCTestCase {
422422
var $testSubgroup: FlagGroupWigwag<SubgroupFlags> {
423423
FlagGroupWigwag(
424424
keyPath: _flagKeyPath,
425-
name: nil,
425+
name: "Test Subgroup",
426426
description: "meow",
427427
displayOption: .navigation,
428428
lookup: _flagLookup
@@ -456,7 +456,7 @@ final class FlagGroupMacroTests: XCTestCase {
456456
var $testSubgroup: FlagGroupWigwag<SubgroupFlags> {
457457
FlagGroupWigwag(
458458
keyPath: _flagKeyPath.append(.customKey("test")),
459-
name: nil,
459+
name: "Test Subgroup",
460460
description: "meow",
461461
displayOption: .navigation,
462462
lookup: _flagLookup

0 commit comments

Comments
 (0)