Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 7d9f835

Browse files
committed
feat: add attribute decorator
Signed-off-by: Yuki Yamamoto <[email protected]>
1 parent f481a51 commit 7d9f835

File tree

2 files changed

+182
-0
lines changed

2 files changed

+182
-0
lines changed
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
#if os(iOS)
2+
import Foundation
3+
import UIKit
4+
5+
/**
6+
Helper class to produce device information attributes
7+
8+
The values appended to the attribute come primarily from the Bundle or UIDevice API
9+
10+
AppInfo contains:
11+
- version: the version name of the app.
12+
- build: the version code of the app.
13+
- namespace: the package name of the app.
14+
15+
DeviceInfo contains:
16+
- manufacturer: the manufacturer of the device.
17+
- model: the model of the device.
18+
- type: the type of the device.
19+
20+
OsInfo contains:
21+
- name: the name of the OS.
22+
- version: the version of the OS.
23+
24+
Locale contains:
25+
- locale: the locale of the device.
26+
- preferred_languages: the preferred languages of the device.
27+
28+
The attributes are only updated when the class is initialized and then static.
29+
*/
30+
public class DeviceInfoAttributeDecorator {
31+
private let staticAttribute: Value
32+
33+
/// - Parameters:
34+
/// - withDeviceInfo: If true, includes device hardware information
35+
/// - withAppInfo: If true, includes application metadata
36+
/// - withOsInfo: If true, includes operating system information
37+
/// - withLocale: If true, includes locale and language preferences
38+
public init(
39+
withDeviceInfo: Bool = false,
40+
withAppInfo: Bool = false,
41+
withOsInfo: Bool = false,
42+
withLocale: Bool = false
43+
) {
44+
var attributes: [String: Value] = [:]
45+
46+
if withDeviceInfo {
47+
let device = UIDevice.current
48+
49+
attributes["device"] = .structure([
50+
"manufacturer": .string("Apple"),
51+
"model": .string(Self.getDeviceModelIdentifier()),
52+
"type": .string(device.model),
53+
])
54+
}
55+
56+
if withAppInfo {
57+
let currentVersion: String = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
58+
let currentBuild: String = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? ""
59+
let bundleId = Bundle.main.bundleIdentifier ?? ""
60+
61+
attributes["app"] = .structure([
62+
"version": .string(currentVersion),
63+
"build": .string(currentBuild),
64+
"namespace": .string(bundleId),
65+
])
66+
}
67+
68+
if withOsInfo {
69+
let device = UIDevice.current
70+
71+
attributes["os"] = .structure([
72+
"name": .string(device.systemName),
73+
"version": .string(device.systemVersion),
74+
])
75+
}
76+
77+
if withLocale {
78+
let locale = Locale.current
79+
let preferredLanguages = Locale.preferredLanguages
80+
81+
// Top level fields
82+
attributes["locale"] = .string(locale.identifier) // Locale identifier (e.g., "en_US")
83+
attributes["preferred_languages"] = .list(preferredLanguages.map { lang in
84+
.string(lang)
85+
})
86+
}
87+
88+
self.staticAttribute = .structure(attributes)
89+
}
90+
91+
/// Returns an attribute where values are decorated (appended) according to the configuration of the `DeviceInfoAttributeDecorator`.
92+
/// Values provided in the `attributes` parameter take precedence over those appended by this class.
93+
public func decorated(attributes attributesToDecorate: [String: Value]) -> [String: Value] {
94+
var result = self.staticAttribute.asStructure() ?? [:]
95+
attributesToDecorate.forEach { (key: String, value: Value) in
96+
result[key] = value
97+
}
98+
return result
99+
}
100+
101+
102+
private static func getDeviceModelIdentifier() -> String {
103+
var systemInfo = utsname()
104+
uname(&systemInfo)
105+
let machineMirror = Mirror(reflecting: systemInfo.machine)
106+
let identifier = machineMirror.children
107+
.compactMap { element in element.value as? Int8 }
108+
.filter { $0 != 0 }
109+
.map {
110+
Character(UnicodeScalar(UInt8($0)))
111+
}
112+
return String(identifier)
113+
}
114+
}
115+
#endif
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import XCTest
2+
3+
@testable import OpenFeature
4+
5+
final class DeviceInfoAttributeDecoratorTests: XCTestCase {
6+
func testEmptyConstructMakesNoOp() {
7+
let result = DeviceInfoAttributeDecorator().decorated(attributes: [:])
8+
XCTAssertEqual(result.count, 0)
9+
}
10+
11+
func testAddDeviceInfo() {
12+
let result = DeviceInfoAttributeDecorator(withDeviceInfo: true).decorated(attributes: [:])
13+
XCTAssertEqual(result.count, 1)
14+
XCTAssertNotNil(result["device"])
15+
XCTAssertNotNil(result["device"]?.asStructure()?["model"])
16+
XCTAssertNotNil(result["device"]?.asStructure()?["type"])
17+
XCTAssertNotNil(result["device"]?.asStructure()?["manufacturer"])
18+
}
19+
20+
func testAddLocale() {
21+
let result = DeviceInfoAttributeDecorator(withLocale: true).decorated(attributes: [:])
22+
XCTAssertEqual(result.count, 2)
23+
XCTAssertNotNil(result["locale"])
24+
XCTAssertNotNil(result["preferred_languages"])
25+
}
26+
27+
func testAddAppInfo() {
28+
let result = DeviceInfoAttributeDecorator(withAppInfo: true).decorated(attributes: [:])
29+
XCTAssertEqual(result.count, 1)
30+
XCTAssertNotNil(result["app"])
31+
XCTAssertNotNil(result["app"]?.asStructure()?["version"])
32+
XCTAssertNotNil(result["app"]?.asStructure()?["build"])
33+
XCTAssertNotNil(result["app"]?.asStructure()?["namespace"])
34+
}
35+
36+
func testAddOsInfo() {
37+
let result = DeviceInfoAttributeDecorator(withOsInfo: true).decorated(attributes: [:])
38+
XCTAssertEqual(result.count, 1)
39+
XCTAssertNotNil(result["os"])
40+
XCTAssertNotNil(result["os"]?.asStructure()?["name"])
41+
XCTAssertNotNil(result["os"]?.asStructure()?["version"])
42+
}
43+
44+
func testAppendsData() {
45+
let result = DeviceInfoAttributeDecorator(
46+
withDeviceInfo: true
47+
).decorated(attributes: ["my_key": .double(42.0)])
48+
XCTAssertEqual(result.count, 2)
49+
XCTAssertEqual(result["my_key"]?.asDouble(), 42.0)
50+
XCTAssertNotNil(result["device"])
51+
}
52+
53+
func testCombinedAttributes() {
54+
let result = DeviceInfoAttributeDecorator(
55+
withDeviceInfo: true,
56+
withAppInfo: true,
57+
withOsInfo: true,
58+
withLocale: true
59+
).decorated(attributes: [:])
60+
61+
XCTAssertEqual(result.count, 5)
62+
XCTAssertNotNil(result["device"])
63+
XCTAssertNotNil(result["app"])
64+
XCTAssertNotNil(result["os"])
65+
XCTAssertNotNil(result["locale"])
66+
}
67+
}

0 commit comments

Comments
 (0)