Skip to content

Commit 61d3b2a

Browse files
committed
Improved performance
1 parent db47a72 commit 61d3b2a

File tree

5 files changed

+146
-34
lines changed

5 files changed

+146
-34
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>classNames</key>
6+
<dict>
7+
<key>DynamicFeatureFlagTests</key>
8+
<dict>
9+
<key>testBenchmarkDynamicJSONPerformance()</key>
10+
<dict>
11+
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
12+
<dict>
13+
<key>baselineAverage</key>
14+
<real>0.393093</real>
15+
<key>baselineIntegrationDisplayName</key>
16+
<string>Local Baseline</string>
17+
</dict>
18+
</dict>
19+
</dict>
20+
</dict>
21+
</dict>
22+
</plist>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>runDestinationsByUUID</key>
6+
<dict>
7+
<key>C1DBDADF-16BC-4146-8CE0-CA4853098245</key>
8+
<dict>
9+
<key>localComputer</key>
10+
<dict>
11+
<key>busSpeedInMHz</key>
12+
<integer>0</integer>
13+
<key>cpuCount</key>
14+
<integer>1</integer>
15+
<key>cpuKind</key>
16+
<string>Apple M4 Pro</string>
17+
<key>cpuSpeedInMHz</key>
18+
<integer>0</integer>
19+
<key>logicalCPUCoresPerPackage</key>
20+
<integer>14</integer>
21+
<key>modelCode</key>
22+
<string>Mac16,7</string>
23+
<key>physicalCPUCoresPerPackage</key>
24+
<integer>14</integer>
25+
<key>platformIdentifier</key>
26+
<string>com.apple.platform.macosx</string>
27+
</dict>
28+
<key>targetArchitecture</key>
29+
<string>arm64</string>
30+
</dict>
31+
</dict>
32+
</dict>
33+
</plist>

Sources/DynamicJSON/DynamicJSON.swift

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,33 @@ public enum DynamicJSON: Decodable {
1919

2020
public init(from decoder: Decoder) throws {
2121
if let container = try? decoder.container(keyedBy: DynamicCodingKeys.self) {
22-
var dict: [String: DynamicJSON] = [:]
23-
for key in container.allKeys {
22+
let dict = try container.allKeys.reduce(into: [String: DynamicJSON]()) { result, key in
2423
let normalized = key.stringValue.normalizedKey()
25-
dict[normalized] = try container.decode(DynamicJSON.self, forKey: key)
24+
result[normalized] = try container.decode(DynamicJSON.self, forKey: key)
2625
}
2726
self = .dictionary(dict)
28-
} else if var arrayContainer = try? decoder.unkeyedContainer() {
27+
return
28+
}
29+
30+
if var arrayContainer = try? decoder.unkeyedContainer() {
2931
var arr: [DynamicJSON] = []
3032
while !arrayContainer.isAtEnd {
31-
arr.append(try arrayContainer.decode(DynamicJSON.self))
33+
if let value = try? arrayContainer.decode(DynamicJSON.self) {
34+
arr.append(value)
35+
}
3236
}
3337
self = .array(arr)
34-
} else if let val = try? decoder.singleValueContainer().decode(Bool.self) {
35-
self = .bool(val)
36-
} else if let val = try? decoder.singleValueContainer().decode(Double.self) {
37-
self = .number(val)
38-
} else if let val = try? decoder.singleValueContainer().decode(String.self) {
39-
self = .string(val)
38+
return
39+
}
40+
41+
let container = try decoder.singleValueContainer()
42+
43+
if let boolVal = try? container.decode(Bool.self) {
44+
self = .bool(boolVal)
45+
} else if let numVal = try? container.decode(Double.self) {
46+
self = .number(numVal)
47+
} else if let strVal = try? container.decode(String.self) {
48+
self = .string(strVal)
4049
} else {
4150
self = .null
4251
}
@@ -70,7 +79,7 @@ public enum DynamicJSON: Decodable {
7079
switch self {
7180
case .bool(let val): return val
7281
case .number(let num): return num != 0
73-
case .string(let str): return ["1", "true", "yes", "on"].contains(str.lowercased())
82+
case .string(let str): return ["1", "true", "yes", "on", "enabled", "active", "authorized", "valid"].contains(str.lowercased())
7483
default: return nil
7584
}
7685
}

Sources/DynamicJSON/Utilities.swift

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,16 @@ extension String {
2424
///
2525
/// Returns: A normalized version of the string for consistent lookup.
2626
func normalizedKey() -> String {
27-
let pattern = #"(?<=[a-z0-9])(?=[A-Z])|[_\-\s]+"#
27+
// Replace hyphens and spaces with underscores for consistent splitting
28+
let sanitized = self
29+
.replacingOccurrences(of: "-", with: "_")
30+
.replacingOccurrences(of: " ", with: "_")
31+
32+
let pattern = #"(?<=[a-z0-9])(?=[A-Z])|_"#
2833
let regex = try! NSRegularExpression(pattern: pattern, options: [])
2934

30-
let range = NSRange(startIndex..<endIndex, in: self)
31-
let spaced = regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: "_")
35+
let range = NSRange(sanitized.startIndex..<sanitized.endIndex, in: sanitized)
36+
let spaced = regex.stringByReplacingMatches(in: sanitized, options: [], range: range, withTemplate: "_")
3237

3338
return spaced
3439
.split(separator: "_")
@@ -48,25 +53,27 @@ extension String {
4853
func levenshteinDistance(to target: String) -> Int {
4954
let source = Array(self)
5055
let target = Array(target)
51-
let (m, n) = (source.count, target.count)
5256

53-
var matrix = Array(repeating: Array(repeating: 0, count: n + 1), count: m + 1)
57+
guard !source.isEmpty else { return target.count }
58+
guard !target.isEmpty else { return source.count }
5459

55-
for i in 0...m { matrix[i][0] = i }
56-
for j in 0...n { matrix[0][j] = j }
60+
var previous = Array(0...target.count)
61+
var current = [Int](repeating: 0, count: target.count + 1)
5762

58-
for i in 1...m {
59-
for j in 1...n {
63+
for i in 1...source.count {
64+
current[0] = i
65+
for j in 1...target.count {
6066
let cost = source[i - 1] == target[j - 1] ? 0 : 1
61-
matrix[i][j] = Swift.min(
62-
matrix[i - 1][j] + 1,
63-
matrix[i][j - 1] + 1,
64-
matrix[i - 1][j - 1] + cost
67+
current[j] = Swift.min(
68+
current[j - 1] + 1, // insertion
69+
previous[j] + 1, // deletion
70+
previous[j - 1] + cost // substitution
6571
)
6672
}
73+
swap(&previous, &current)
6774
}
6875

69-
return matrix[m][n]
76+
return previous[target.count]
7077
}
7178
}
7279

@@ -85,19 +92,29 @@ extension Dictionary where Key == String, Value == DynamicJSON {
8592
func fuzzyMatch(for key: String, logMatch: (_ original: String, _ matched: String) -> Void) -> DynamicJSON? {
8693
let normalized = key.normalizedKey()
8794

95+
// Try partial match
8896
if let partial = self.first(where: { $0.key.contains(normalized) }) {
8997
logMatch(key, partial.key)
9098
return partial.value
9199
}
92100

101+
// Fuzzy match
93102
let maxDistance = 2
94-
let best = self.map { ($0.key, $0.value, normalized.levenshteinDistance(to: $0.key)) }
95-
.filter { $0.2 <= maxDistance }
96-
.sorted(by: { $0.2 < $1.2 })
97-
.first
103+
var bestMatchKey: String?
104+
var bestMatchValue: DynamicJSON?
105+
var bestDistance = Int.max
106+
107+
for (storedKey, value) in self {
108+
let distance = normalized.levenshteinDistance(to: storedKey)
109+
if distance <= maxDistance && distance < bestDistance {
110+
bestDistance = distance
111+
bestMatchKey = storedKey
112+
bestMatchValue = value
113+
}
114+
}
98115

99-
if let (matchKey, value, _) = best {
100-
logMatch(key, matchKey)
116+
if let key = bestMatchKey, let value = bestMatchValue {
117+
logMatch(key, key)
101118
return value
102119
}
103120

Tests/DynamicJSONTests/DynamicJSONTests.swift

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import Testing
2+
import XCTest
23
import Foundation
34
@testable import DynamicJSON
45

5-
final class DynamicFeatureFlagTests {
6+
final class DynamicFeatureFlagTests: XCTestCase {
67

7-
@Test func testDynamicJSON() throws {
8+
func testDynamicJSON() throws {
89
let json = """
910
{
1011
"createdAt": "2025-11-15T10:00:00Z",
@@ -76,5 +77,35 @@ final class DynamicFeatureFlagTests {
7677
#expect(dynamicJSONModel.flags["new ui"].bool == true) // space
7778
#expect(dynamicJSONModel.flags.beta_feature.bool == false) // normalized
7879
}
80+
81+
func testBenchmarkDynamicJSONPerformance() throws {
82+
83+
let decoder = JSONDecoder()
84+
// Simulate a moderately large JSON
85+
let jsonString = (0..<1000).map {
86+
"""
87+
"feature_\($0)": {
88+
"enabled": \($0 % 2 == 0),
89+
"rollout": "\($0 % 100)"
90+
}
91+
"""
92+
}.joined(separator: ",\n")
93+
94+
let wrapped = "{ \(jsonString) }"
95+
guard let data = wrapped.data(using: .utf8) else {
96+
return
97+
}
98+
99+
// Run benchmark
100+
measure {
101+
let decoded = try? decoder.decode(DynamicJSON.self, from: data)
102+
#expect(decoded != nil)
103+
104+
// Test accessing and converting various keys
105+
let sampleKey = "feature_123"
106+
let value = decoded?[sampleKey].dictionary?["enabled"]?.bool
107+
#expect(value != nil)
108+
}
109+
}
79110
}
80111

0 commit comments

Comments
 (0)