diff --git a/Makefile b/Makefile index 07cb28af57..044b1766c0 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ tools: @echo "OK πŸ‘Œ" # The release version of `dd-sdk-swift-testing` to use for tests instrumentation. -DD_SDK_SWIFT_TESTING_VERSION = 0.5.1 +DD_SDK_SWIFT_TESTING_VERSION = 0.6.0 define DD_SDK_TESTING_XCCONFIG_CI FRAMEWORK_SEARCH_PATHS=$$(inherited) $$(SRCROOT)/../instrumented-tests/DatadogSDKTesting.xcframework/ios-arm64_x86_64-simulator/\n diff --git a/Package.swift b/Package.swift index 385a73e2e7..0bad96217a 100644 --- a/Package.swift +++ b/Package.swift @@ -16,6 +16,14 @@ let package = Package( name: "DatadogObjc", type: .dynamic, targets: ["DatadogObjc"]), + .library( + name: "DatadogStatic", + type: .static, + targets: ["Datadog"]), + .library( + name: "DatadogStaticObjc", + type: .static, + targets: ["DatadogObjc"]), ], dependencies: [ .package(url: "https://github.com/lyft/Kronos.git", .upToNextMinor(from: "4.1.0")) diff --git a/README.md b/README.md index 70502ffe03..66c787fc57 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,12 @@ See [Datadog iOS Trace Collection](https://docs.datadoghq.com/tracing/setup/ios/ ![Datadog iOS Log Collection](docs/images/tracing.png) +### RUM Events Collection + +See [Datadog iOS RUM Collection](https://docs.datadoghq.com/real_user_monitoring/ios/?tab=us) documentation to try it out. + +![Datadog iOS RUM Collection](docs/images/rum.png) + ## Integrations ### Alamofire diff --git a/Sources/Datadog/Core/Utils/JSONEncoder.swift b/Sources/Datadog/Core/Utils/JSONEncoder.swift index ebb45e79bb..f6a2ae407b 100644 --- a/Sources/Datadog/Core/Utils/JSONEncoder.swift +++ b/Sources/Datadog/Core/Utils/JSONEncoder.swift @@ -15,25 +15,7 @@ extension JSONEncoder { try container.encode(formatted) } if #available(iOS 13.0, OSX 10.15, *) { - // NOTE: The `.sortedKeys` option was added in RUMM-776 after discovering an issue - // with backend processing of the RUM View payloads. The custom timings encoding for - // RUM views requires following structure: - // - // ``` - // { - // view: { /* serialized, auto-generated RUM view event */ }, - // view.custom_timings.: , - // view.custom_timings.: - // ... - // } - // ``` - // - // To guarantee proper backend-side processing, the `view.custom_timings` keys must be - // encoded after the `view` object. Using `.sortedKeys` enforces this order. - // - encoder.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] - } else { - encoder.outputFormatting = [.sortedKeys] + encoder.outputFormatting = [.withoutEscapingSlashes] } return encoder } diff --git a/Sources/Datadog/RUM/DataModels/RUMDataModels.swift b/Sources/Datadog/RUM/DataModels/RUMDataModels.swift index 717c2c1a6a..83895fad2b 100644 --- a/Sources/Datadog/RUM/DataModels/RUMDataModels.swift +++ b/Sources/Datadog/RUM/DataModels/RUMDataModels.swift @@ -108,6 +108,9 @@ public struct RUMViewEvent: RUMDataModel { /// Total layout shift score that occured on the view public let cumulativeLayoutShift: Double? + /// User custom timings of the view. As timing name is used as facet path, it must contain only letters, digits, or the characters - _ . @ $ + public let customTimings: [String: Int64]? + /// Duration in ns to the complete parsing and loading of the document and its sub resources public let domComplete: Int64? @@ -150,6 +153,9 @@ public struct RUMViewEvent: RUMDataModel { /// Properties of the long tasks of the view public let longTask: LongTask? + /// User defined name of the view + public var name: String? + /// URL that linked to the initial view of the page public var referrer: String? @@ -166,6 +172,7 @@ public struct RUMViewEvent: RUMDataModel { case action = "action" case crash = "crash" case cumulativeLayoutShift = "cumulative_layout_shift" + case customTimings = "custom_timings" case domComplete = "dom_complete" case domContentLoaded = "dom_content_loaded" case domInteractive = "dom_interactive" @@ -180,6 +187,7 @@ public struct RUMViewEvent: RUMDataModel { case loadingTime = "loading_time" case loadingType = "loading_type" case longTask = "long_task" + case name = "name" case referrer = "referrer" case resource = "resource" case timeSpent = "time_spent" @@ -562,6 +570,9 @@ public struct RUMResourceEvent: RUMDataModel { /// UUID of the view public let id: String + /// User defined name of the view + public var name: String? + /// URL that linked to the initial view of the page public var referrer: String? @@ -570,6 +581,7 @@ public struct RUMResourceEvent: RUMDataModel { enum CodingKeys: String, CodingKey { case id = "id" + case name = "name" case referrer = "referrer" case url = "url" } @@ -769,6 +781,9 @@ public struct RUMActionEvent: RUMDataModel { /// UUID of the view public let id: String + /// User defined name of the view + public var name: String? + /// URL that linked to the initial view of the page public var referrer: String? @@ -777,6 +792,7 @@ public struct RUMActionEvent: RUMDataModel { enum CodingKeys: String, CodingKey { case id = "id" + case name = "name" case referrer = "referrer" case url = "url" } @@ -990,6 +1006,9 @@ public struct RUMErrorEvent: RUMDataModel { /// UUID of the view public let id: String + /// User defined name of the view + public var name: String? + /// URL that linked to the initial view of the page public var referrer: String? @@ -998,6 +1017,7 @@ public struct RUMErrorEvent: RUMDataModel { enum CodingKeys: String, CodingKey { case id = "id" + case name = "name" case referrer = "referrer" case url = "url" } @@ -1083,4 +1103,4 @@ public enum RUMMethod: String, Codable { case patch = "PATCH" } -// Generated from https://github.com/DataDog/rum-events-format/tree/8b955a03d0fe0b2f032a02d6800c61ef3fc9fada +// Generated from https://github.com/DataDog/rum-events-format/tree/a37c41a4ac1aa3bfdc8d1fcecb35e4d1e07adddc diff --git a/Sources/Datadog/RUM/RUMEvent/RUMEventBuilder.swift b/Sources/Datadog/RUM/RUMEvent/RUMEventBuilder.swift index eefbd566d6..8f1f5c9253 100644 --- a/Sources/Datadog/RUM/RUMEvent/RUMEventBuilder.swift +++ b/Sources/Datadog/RUM/RUMEvent/RUMEventBuilder.swift @@ -15,14 +15,12 @@ internal class RUMEventBuilder { func createRUMEvent( with model: DM, - attributes: [String: Encodable], - customTimings: [String: Int64]? = nil + attributes: [String: Encodable] ) -> RUMEvent { return RUMEvent( model: model, attributes: attributes, - userInfoAttributes: userInfoProvider.value.extraInfo, - customViewTimings: customTimings + userInfoAttributes: userInfoProvider.value.extraInfo ) } } diff --git a/Sources/Datadog/RUM/RUMEvent/RUMEventEncoder.swift b/Sources/Datadog/RUM/RUMEvent/RUMEventEncoder.swift index 1f1858d20e..b86630d4c7 100644 --- a/Sources/Datadog/RUM/RUMEvent/RUMEventEncoder.swift +++ b/Sources/Datadog/RUM/RUMEvent/RUMEventEncoder.swift @@ -16,9 +16,6 @@ internal struct RUMEvent: Encodable { var attributes: [String: Encodable] var userInfoAttributes: [String: Encodable] - /// Custom View timings (only available if `DM` is a RUM View model) - var customViewTimings: [String: Int64]? - func encode(to encoder: Encoder) throws { let sanitizedEvent = RUMEventSanitizer().sanitize(event: self) try RUMEventEncoder().encode(sanitizedEvent, to: encoder) @@ -45,9 +42,6 @@ internal struct RUMEventEncoder { try event.userInfoAttributes.forEach { attributeName, attributeValue in try attributesContainer.encode(EncodableValue(attributeValue), forKey: DynamicCodingKey("context.usr.\(attributeName)")) } - try event.customViewTimings?.forEach { timingName, timingDuration in - try attributesContainer.encode(timingDuration, forKey: DynamicCodingKey("view.custom_timings.\(timingName)")) - } // Encode `RUMDataModel` try event.model.encode(to: encoder) diff --git a/Sources/Datadog/RUM/RUMEvent/RUMEventSanitizer.swift b/Sources/Datadog/RUM/RUMEvent/RUMEventSanitizer.swift index 4f1cac7009..a94888b6b2 100644 --- a/Sources/Datadog/RUM/RUMEvent/RUMEventSanitizer.swift +++ b/Sources/Datadog/RUM/RUMEvent/RUMEventSanitizer.swift @@ -12,32 +12,24 @@ internal struct RUMEventSanitizer { func sanitize(event: RUMEvent) -> RUMEvent { // Sanitize attribute names - var sanitizedTimings = event.customViewTimings.flatMap { attributesSanitizer.sanitizeKeys(for: $0) } var sanitizedUserExtraInfo = attributesSanitizer.sanitizeKeys(for: event.userInfoAttributes) var sanitizedAttributes = attributesSanitizer.sanitizeKeys(for: event.attributes) // Limit to max number of attributes. // If any attributes need to be removed, we first reduce number of - // event attributes, then user info extra attributes, then custom timings. - sanitizedTimings = sanitizedTimings.flatMap { timings in - attributesSanitizer.limitNumberOf( - attributes: timings, - to: AttributesSanitizer.Constraints.maxNumberOfAttributes - ) - } + // event attributes, then user info extra attributes. sanitizedUserExtraInfo = attributesSanitizer.limitNumberOf( attributes: sanitizedUserExtraInfo, - to: AttributesSanitizer.Constraints.maxNumberOfAttributes - (sanitizedTimings?.count ?? 0) + to: AttributesSanitizer.Constraints.maxNumberOfAttributes ) sanitizedAttributes = attributesSanitizer.limitNumberOf( attributes: sanitizedAttributes, - to: AttributesSanitizer.Constraints.maxNumberOfAttributes - (sanitizedTimings?.count ?? 0) - sanitizedUserExtraInfo.count + to: AttributesSanitizer.Constraints.maxNumberOfAttributes - sanitizedUserExtraInfo.count ) var sanitizedEvent = event sanitizedEvent.attributes = sanitizedAttributes sanitizedEvent.userInfoAttributes = sanitizedUserExtraInfo - sanitizedEvent.customViewTimings = sanitizedTimings return sanitizedEvent } } diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift index a94f399835..bd5903764c 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift @@ -270,6 +270,7 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { action: .init(count: actionsCount.toInt64), crash: nil, cumulativeLayoutShift: nil, + customTimings: customTimings, domComplete: nil, domContentLoaded: nil, domInteractive: nil, @@ -291,7 +292,7 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { ) ) - let event = dependencies.eventBuilder.createRUMEvent(with: eventData, attributes: attributes, customTimings: customTimings) + let event = dependencies.eventBuilder.createRUMEvent(with: eventData, attributes: attributes) dependencies.eventOutput.write(rumEvent: event) } diff --git a/Sources/DatadogObjc/RUM/RUMDataModels+objc.swift b/Sources/DatadogObjc/RUM/RUMDataModels+objc.swift index 47133619fa..0bde95fa8d 100644 --- a/Sources/DatadogObjc/RUM/RUMDataModels+objc.swift +++ b/Sources/DatadogObjc/RUM/RUMDataModels+objc.swift @@ -271,6 +271,10 @@ public class DDRUMViewEventView: NSObject { root.swiftModel.view.cumulativeLayoutShift as NSNumber? } + @objc public var customTimings: [String: NSNumber]? { + root.swiftModel.view.customTimings as [String: NSNumber]? + } + @objc public var domComplete: NSNumber? { root.swiftModel.view.domComplete as NSNumber? } @@ -327,6 +331,11 @@ public class DDRUMViewEventView: NSObject { root.swiftModel.view.longTask != nil ? DDRUMViewEventViewLongTask(root: root) : nil } + @objc public var name: String? { + set { root.swiftModel.view.name = newValue } + get { root.swiftModel.view.name } + } + @objc public var referrer: String? { set { root.swiftModel.view.referrer = newValue } get { root.swiftModel.view.referrer } @@ -1056,6 +1065,11 @@ public class DDRUMResourceEventView: NSObject { root.swiftModel.view.id } + @objc public var name: String? { + set { root.swiftModel.view.name = newValue } + get { root.swiftModel.view.name } + } + @objc public var referrer: String? { set { root.swiftModel.view.referrer = newValue } get { root.swiftModel.view.referrer } @@ -1461,6 +1475,11 @@ public class DDRUMActionEventView: NSObject { root.swiftModel.view.id } + @objc public var name: String? { + set { root.swiftModel.view.name = newValue } + get { root.swiftModel.view.name } + } + @objc public var referrer: String? { set { root.swiftModel.view.referrer = newValue } get { root.swiftModel.view.referrer } @@ -1949,6 +1968,11 @@ public class DDRUMErrorEventView: NSObject { root.swiftModel.view.id } + @objc public var name: String? { + set { root.swiftModel.view.name = newValue } + get { root.swiftModel.view.name } + } + @objc public var referrer: String? { set { root.swiftModel.view.referrer = newValue } get { root.swiftModel.view.referrer } @@ -1962,4 +1986,4 @@ public class DDRUMErrorEventView: NSObject { // swiftlint:enable force_unwrapping -// Generated from https://github.com/DataDog/rum-events-format/tree/8b955a03d0fe0b2f032a02d6800c61ef3fc9fada +// Generated from https://github.com/DataDog/rum-events-format/tree/a37c41a4ac1aa3bfdc8d1fcecb35e4d1e07adddc diff --git a/Tests/DatadogBenchmarkTests/DataStorage/RUMStorageBenchmarkTests.swift b/Tests/DatadogBenchmarkTests/DataStorage/RUMStorageBenchmarkTests.swift index c9a6e6652b..5f57f35c38 100644 --- a/Tests/DatadogBenchmarkTests/DataStorage/RUMStorageBenchmarkTests.swift +++ b/Tests/DatadogBenchmarkTests/DataStorage/RUMStorageBenchmarkTests.swift @@ -99,6 +99,7 @@ class RUMStorageBenchmarkTests: XCTestCase { action: .init(count: .mockAny()), crash: .init(count: .mockAny()), cumulativeLayoutShift: nil, + customTimings: .mockAny(), domComplete: nil, domContentLoaded: nil, domInteractive: nil, @@ -120,8 +121,7 @@ class RUMStorageBenchmarkTests: XCTestCase { ) ), attributes: ["attribute": "value"], - userInfoAttributes: ["str": "value", "int": 11_235, "bool": true], - customViewTimings: nil + userInfoAttributes: ["str": "value", "int": 11_235, "bool": true] ) } } diff --git a/Tests/DatadogTests/Datadog/Core/Utils/JSONEncoderTests.swift b/Tests/DatadogTests/Datadog/Core/Utils/JSONEncoderTests.swift index dfa74a9df4..5e4fe43f8f 100644 --- a/Tests/DatadogTests/Datadog/Core/Utils/JSONEncoderTests.swift +++ b/Tests/DatadogTests/Datadog/Core/Utils/JSONEncoderTests.swift @@ -29,31 +29,4 @@ class JSONEncoderTests: XCTestCase { XCTAssertEqual(encodedURL.utf8String, #"{"value":"https:\/\/example.com\/foo"}"#) } } - - func testWhenEncoding_thenKeysFollowLexicographicOrder() throws { - struct Foo: Codable { - var one = 1 - var two = 1 - var three = 1 - var four = 1 - var five = 1 - - enum CodingKeys: String, CodingKey { - case one = "aaaaaa" - case two = "bb" - case three = "aaa" - case four = "bbb" - case five = "aaa.aaa" - } - } - - // When - let encodedFoo = try jsonEncoder.encode(Foo()) - - // Then - XCTAssertEqual( - encodedFoo.utf8String, - #"{"aaa":1,"aaa.aaa":1,"aaaaaa":1,"bb":1,"bbb":1}"# - ) - } } diff --git a/Tests/DatadogTests/Datadog/Mocks/RUMDataModelMocks.swift b/Tests/DatadogTests/Datadog/Mocks/RUMDataModelMocks.swift index 9a58bfc262..a0a15effb3 100644 --- a/Tests/DatadogTests/Datadog/Mocks/RUMDataModelMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/RUMDataModelMocks.swift @@ -60,6 +60,7 @@ extension RUMViewEvent { action: .init(count: .mockRandom()), crash: .init(count: .mockRandom()), cumulativeLayoutShift: .mockRandom(), + customTimings: .mockAny(), domComplete: .mockRandom(), domContentLoaded: .mockRandom(), domInteractive: .mockRandom(), diff --git a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift index 6e7ff86b73..7070bfcc18 100644 --- a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift @@ -88,14 +88,12 @@ extension RUMEvent { static func mockWith( model: DM, attributes: [String: Encodable] = [:], - userInfoAttributes: [String: Encodable] = [:], - customViewTimings: [String: Int64]? = nil + userInfoAttributes: [String: Encodable] = [:] ) -> RUMEvent { return RUMEvent( model: model, attributes: attributes, - userInfoAttributes: userInfoAttributes, - customViewTimings: customViewTimings + userInfoAttributes: userInfoAttributes ) } @@ -106,17 +104,10 @@ extension RUMEvent { return attributes } - func randomTimings() -> [String: Int64] { - var timings: [String: Int64] = [:] - (0..<10).forEach { index in timings["timing\(index)"] = .mockRandom() } - return timings - } - return RUMEvent( model: model, attributes: randomAttributes(prefixed: "event-attribute"), - userInfoAttributes: randomAttributes(prefixed: "user-attribute"), - customViewTimings: randomTimings() + userInfoAttributes: randomAttributes(prefixed: "user-attribute") ) } } @@ -502,13 +493,19 @@ extension RUMViewScope { return mockWith() } + static func randomTimings() -> [String: Int64] { + var timings: [String: Int64] = [:] + (0..<10).forEach { index in timings["timing\(index)"] = .mockRandom() } + return timings + } + static func mockWith( parent: RUMContextProvider = RUMContextProviderMock(), dependencies: RUMScopeDependencies = .mockAny(), identity: RUMViewIdentifiable = mockView, uri: String = .mockAny(), attributes: [AttributeKey: AttributeValue] = [:], - customTimings: [String: Int64] = [:], + customTimings: [String: Int64] = randomTimings(), startTime: Date = .mockAny() ) -> RUMViewScope { return RUMViewScope( diff --git a/Tests/DatadogTests/Datadog/Mocks/SystemFrameworks/FoundationMocks.swift b/Tests/DatadogTests/Datadog/Mocks/SystemFrameworks/FoundationMocks.swift index bd16b1f698..0a3f137434 100644 --- a/Tests/DatadogTests/Datadog/Mocks/SystemFrameworks/FoundationMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/SystemFrameworks/FoundationMocks.swift @@ -33,7 +33,15 @@ import Foundation // MARK: - Basic types -extension Data { +protocol AnyMockable { + static func mockAny() -> Self +} + +protocol RandomMockable { + static func mockRandom() -> Self +} + +extension Data: AnyMockable { static func mockAny() -> Data { return Data() } @@ -71,7 +79,13 @@ extension Array { } } -extension Date { +extension Dictionary: AnyMockable where Key: AnyMockable, Value: AnyMockable { + static func mockAny() -> Dictionary { + return [Key.mockAny(): Value.mockAny()] + } +} + +extension Date: AnyMockable { static func mockAny() -> Date { return Date(timeIntervalSinceReferenceDate: 1) } @@ -99,7 +113,7 @@ extension Date { } } -extension TimeZone { +extension TimeZone: AnyMockable { static var UTC: TimeZone { TimeZone(abbreviation: "UTC")! } static var EET: TimeZone { TimeZone(abbreviation: "EET")! } static func mockAny() -> TimeZone { .EET } @@ -111,7 +125,7 @@ extension Calendar { } } -extension URL { +extension URL: AnyMockable, RandomMockable { static func mockAny() -> URL { return URL(string: "https://www.datadoghq.com")! } @@ -131,7 +145,7 @@ extension URL { } } -extension String { +extension String: AnyMockable { static func mockAny() -> String { return "abc" } @@ -153,36 +167,40 @@ extension String { } } -extension Int { +extension Int: AnyMockable { static func mockAny() -> Int { return 0 } } -extension Int64 { +extension Int64: AnyMockable, RandomMockable { static func mockAny() -> Int64 { 0 } static func mockRandom() -> Int64 { Int64.random(in: Int64.min.. UInt64 { return 0 } } -extension Bool { +extension Bool: AnyMockable { static func mockAny() -> Bool { return false } } -extension Float { +extension Float: AnyMockable { static func mockAny() -> Float { return 0 } } -extension Double { +extension Double: AnyMockable, RandomMockable { + static func mockAny() -> Float { + return 0 + } + static func mockRandom() -> Double { return Double.random(in: 0.. URLRequest { return URLRequest(url: .mockAny()) } diff --git a/Tests/DatadogTests/Datadog/RUM/RUMEvent/RUMEventSanitizerTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMEvent/RUMEventSanitizerTests.swift index 749dc19cb7..8b2be3f279 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMEvent/RUMEventSanitizerTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMEvent/RUMEventSanitizerTests.swift @@ -44,20 +44,6 @@ class RUMEventSanitizerTests: XCTestCase { "user-info-one.two.three.four.five.six.seven.eight.nine.ten": mockValue(), "user-info-one.two.three.four.five.six.seven.eight.nine.ten.eleven": mockValue(), "user-info-one.two.three.four.five.six.seven.eight.nine.ten.eleven.twelve": mockValue(), - ], - customViewTimings: [ - "timing-one": .mockRandom(), - "timing-one.two": .mockRandom(), - "timing-one.two.three": .mockRandom(), - "timing-one.two.three.four": .mockRandom(), - "timing-one.two.three.four.five": .mockRandom(), - "timing-one.two.three.four.five.six": .mockRandom(), - "timing-one.two.three.four.five.six.seven": .mockRandom(), - "timing-one.two.three.four.five.six.seven.eight": .mockRandom(), - "timing-one.two.three.four.five.six.seven.eight.nine": .mockRandom(), - "timing-one.two.three.four.five.six.seven.eight.nine.ten": .mockRandom(), - "timing-one.two.three.four.five.six.seven.eight.nine.ten.eleven": .mockRandom(), - "timing-one.two.three.four.five.six.seven.eight.nine.ten.eleven.twelve": .mockRandom(), ] ) @@ -90,19 +76,6 @@ class RUMEventSanitizerTests: XCTestCase { XCTAssertNotNil(sanitized.userInfoAttributes["user-info-one.two.three.four.five.six.seven.eight_nine_ten"]) XCTAssertNotNil(sanitized.userInfoAttributes["user-info-one.two.three.four.five.six.seven.eight_nine_ten_eleven"]) XCTAssertNotNil(sanitized.userInfoAttributes["user-info-one.two.three.four.five.six.seven.eight_nine_ten_eleven_twelve"]) - - XCTAssertEqual(sanitized.customViewTimings?.count, 12) - XCTAssertNotNil(sanitized.customViewTimings?["timing-one"]) - XCTAssertNotNil(sanitized.customViewTimings?["timing-one.two"]) - XCTAssertNotNil(sanitized.customViewTimings?["timing-one.two.three"]) - XCTAssertNotNil(sanitized.customViewTimings?["timing-one.two.three.four"]) - XCTAssertNotNil(sanitized.customViewTimings?["timing-one.two.three.four.five"]) - XCTAssertNotNil(sanitized.customViewTimings?["timing-one.two.three.four.five.six"]) - XCTAssertNotNil(sanitized.customViewTimings?["timing-one.two.three.four.five.six.seven"]) - XCTAssertNotNil(sanitized.customViewTimings?["timing-one.two.three.four.five.six.seven.eight"]) - XCTAssertNotNil(sanitized.customViewTimings?["timing-one.two.three.four.five.six.seven.eight_nine_ten"]) - XCTAssertNotNil(sanitized.customViewTimings?["timing-one.two.three.four.five.six.seven.eight_nine_ten_eleven"]) - XCTAssertNotNil(sanitized.customViewTimings?["timing-one.two.three.four.five.six.seven.eight_nine_ten_eleven_twelve"]) } test(model: viewEvent) @@ -113,12 +86,11 @@ class RUMEventSanitizerTests: XCTestCase { func testWhenNumberOfAttributesExceedsLimit_itDropsExtraOnes() { func test(model: DM) { - let oneThirdOfTheLimit = Int(Double(AttributesSanitizer.Constraints.maxNumberOfAttributes) * 0.34) - let tripleTheLimit = AttributesSanitizer.Constraints.maxNumberOfAttributes * 3 + let oneHalfOfTheLimit = Int(Double(AttributesSanitizer.Constraints.maxNumberOfAttributes) * 0.5) + let twiceTheLimit = AttributesSanitizer.Constraints.maxNumberOfAttributes * 2 - let numberOfAttributes: Int = .random(in: oneThirdOfTheLimit...tripleTheLimit) - let numberOfUserInfoAttributes: Int = .random(in: oneThirdOfTheLimit...tripleTheLimit) - let numberOfTimings: Int = .random(in: oneThirdOfTheLimit...tripleTheLimit) + let numberOfAttributes: Int = .random(in: oneHalfOfTheLimit...twiceTheLimit) + let numberOfUserInfoAttributes: Int = .random(in: oneHalfOfTheLimit...twiceTheLimit) let mockAttributes = (0..( model: model, attributes: Dictionary(uniqueKeysWithValues: mockAttributes), - userInfoAttributes: Dictionary(uniqueKeysWithValues: mockUserInfoAttributes), - customViewTimings: Dictionary(uniqueKeysWithValues: mockTimings) + userInfoAttributes: Dictionary(uniqueKeysWithValues: mockUserInfoAttributes) ) // When @@ -142,15 +110,12 @@ class RUMEventSanitizerTests: XCTestCase { // Then var remaining = AttributesSanitizer.Constraints.maxNumberOfAttributes - let expectedSanitizedCustomTimings = min(sanitized.customViewTimings!.count, remaining) - remaining -= expectedSanitizedCustomTimings let expectedSanitizedUserInfo = min(sanitized.userInfoAttributes.count, remaining) remaining -= expectedSanitizedUserInfo let expectedSanitizedAttrs = min(sanitized.attributes.count, remaining) remaining -= expectedSanitizedAttrs XCTAssertGreaterThanOrEqual(remaining, 0) - XCTAssertEqual(sanitized.customViewTimings!.count, expectedSanitizedCustomTimings, "If number of attributes needs to be limited, `customViewTimings` are removed last") XCTAssertEqual(sanitized.userInfoAttributes.count, expectedSanitizedUserInfo, "If number of attributes needs to be limited, `userInfoAttributes` are removed second") XCTAssertEqual(sanitized.attributes.count, expectedSanitizedAttrs, "If number of attributes needs to be limited, `attributes` are removed first.") } diff --git a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift index 35a485fade..45581d22fe 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift @@ -607,13 +607,13 @@ class RUMViewScopeTests: XCTestCase { let events = try XCTUnwrap(output.recordedEvents(ofType: RUMEvent.self)) XCTAssertEqual(events.count, 3, "There should be 3 View updates sent") - XCTAssertEqual(events[0].customViewTimings, [:]) + XCTAssertEqual(events[0].model.view.customTimings, [:]) XCTAssertEqual( - events[1].customViewTimings, + events[1].model.view.customTimings, ["timing-after-500000000ns": 500_000_000] ) XCTAssertEqual( - events[2].customViewTimings, + events[2].model.view.customTimings, ["timing-after-500000000ns": 500_000_000, "timing-after-1000000000ns": 1_000_000_000] ) } @@ -648,7 +648,7 @@ class RUMViewScopeTests: XCTestCase { // Then let lastEvent = try XCTUnwrap(output.recordedEvents(ofType: RUMEvent.self).last) - XCTAssertEqual(lastEvent.customViewTimings, [:]) + XCTAssertEqual(lastEvent.model.view.customTimings, [:]) } // MARK: - Dates Correction diff --git a/Tests/DatadogTests/Datadog/RUM/Scrubbing/RUMEventsMapperTests.swift b/Tests/DatadogTests/Datadog/RUM/Scrubbing/RUMEventsMapperTests.swift index eaf5f485cf..4260ddf6bd 100644 --- a/Tests/DatadogTests/Datadog/RUM/Scrubbing/RUMEventsMapperTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/Scrubbing/RUMEventsMapperTests.swift @@ -76,19 +76,15 @@ class RUMEventsMapperTests: XCTestCase { // Then XCTAssertEqual(rumEvent1.attributes as! [String: String], mappedRUMEvent1.attributes as! [String: String]) XCTAssertEqual(rumEvent1.userInfoAttributes as! [String: String], mappedRUMEvent1.userInfoAttributes as! [String: String]) - XCTAssertEqual(rumEvent1.customViewTimings, mappedRUMEvent1.customViewTimings) XCTAssertEqual(rumEvent2.attributes as! [String: String], mappedRUMEvent2.attributes as! [String: String]) XCTAssertEqual(rumEvent2.userInfoAttributes as! [String: String], mappedRUMEvent2.userInfoAttributes as! [String: String]) - XCTAssertEqual(rumEvent2.customViewTimings, mappedRUMEvent2.customViewTimings) XCTAssertEqual(rumEvent3.attributes as! [String: String], mappedRUMEvent3.attributes as! [String: String]) XCTAssertEqual(rumEvent3.userInfoAttributes as! [String: String], mappedRUMEvent3.userInfoAttributes as! [String: String]) - XCTAssertEqual(rumEvent3.customViewTimings, mappedRUMEvent3.customViewTimings) XCTAssertEqual(rumEvent4.attributes as! [String: String], mappedRUMEvent4.attributes as! [String: String]) XCTAssertEqual(rumEvent4.userInfoAttributes as! [String: String], mappedRUMEvent4.userInfoAttributes as! [String: String]) - XCTAssertEqual(rumEvent4.customViewTimings, mappedRUMEvent4.customViewTimings) } func testGivenMappersEnabled_whenDroppingEvents_itReturnsNil() { diff --git a/Tests/DatadogTests/Datadog/TracerTests.swift b/Tests/DatadogTests/Datadog/TracerTests.swift index 0255a3d1fb..bb3e35eef1 100644 --- a/Tests/DatadogTests/Datadog/TracerTests.swift +++ b/Tests/DatadogTests/Datadog/TracerTests.swift @@ -583,7 +583,7 @@ class TracerTests: XCTestCase { ) XCTAssertEqual( try spanMatcher.meta.custom(keyPath: "meta.person"), - #"{"age":30,"name":"Adam","nationality":"Polish"}"# + #"{"name":"Adam","age":30,"nationality":"Polish"}"# ) XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.nested.string"), "hello") XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.url"), "https://example.com/image.png") diff --git a/docs/README.md b/docs/README.md index c3934967df..297da221d7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,5 +2,7 @@ Find in this folder dedicated documentation for: +* [Tracking consent API (GDPR)](gdpr.md). * [Collecting and sending logs from your iOS application to Datadog](log_collection.md). * [Collecting and sending traces from your iOS application to Datadog](trace_collection.md). +* [Collecting and sending RUM events from your iOS application to Datadog](rum_collection.md). diff --git a/docs/gdpr.md b/docs/gdpr.md new file mode 100644 index 0000000000..fc31255af8 --- /dev/null +++ b/docs/gdpr.md @@ -0,0 +1,18 @@ +# Tracking Consent + +### Tracking consent values + +To be compliant with the GDPR regulation, the SDK requires the tracking consent value at initialization. +The `trackingConsent` can be one of the following values: + +1. `TrackingConsent.pending` - the SDK starts collecting and batching the data but does not send it to Datadog. The SDK waits for the new tracking consent value to decide what to do with the batched data. +2. `TrackingConsent.granted` - the SDK starts collecting the data and sends it to Datadog. +3. `TrackingConsent.notGranted` - the SDK does not collect any data: logs, traces, and RUM events are not sent to Datadog. + +### Updating the tracking consent at runtime + +To change the tracking consent value after the SDK is initialized, use the `Datadog.set(trackingConsent:)` API call. +The SDK changes its behavior according to the new value. For example, if the current tracking consent is `.pending`: + +- if changed to `.granted`, the SDK will send all current and future data to Datadog; +- if changed to `.notGranted`, the SDK will wipe all current data and will not collect any future data. diff --git a/docs/images/rum.png b/docs/images/rum.png new file mode 100644 index 0000000000..ab12b16955 Binary files /dev/null and b/docs/images/rum.png differ diff --git a/docs/log_collection.md b/docs/log_collection.md index 573d2f45fc..0359c1b2ec 100644 --- a/docs/log_collection.md +++ b/docs/log_collection.md @@ -52,6 +52,7 @@ github "DataDog/dd-sdk-ios" ```swift Datadog.initialize( appContext: .init(), + trackingConsent: trackingConsent, configuration: Datadog.Configuration .builderUsing(clientToken: "", environment: "") .set(serviceName: "app-name") @@ -65,6 +66,7 @@ Datadog.initialize( ```swift Datadog.initialize( appContext: .init(), + trackingConsent: trackingConsent, configuration: Datadog.Configuration .builderUsing(clientToken: "", environment: "") .set(serviceName: "app-name") @@ -76,6 +78,19 @@ Datadog.initialize( {{% /tab %}} {{< /tabs >}} + To be compliant with the GDPR regulation, the SDK requires the `trackingConsent` value at initialization. + The `trackingConsent` can be one of the following values: + + - `.pending` - the SDK starts collecting and batching the data but does not send it to Datadog. The SDK waits for the new tracking consent value to decide what to do with the batched data. + - `.granted` - the SDK starts collecting the data and sends it to Datadog. + - `.notGranted` - the SDK does not collect any data: logs, traces, and RUM events are not sent to Datadog. + + To change the tracking consent value after the SDK is initialized, use the `Datadog.set(trackingConsent:)` API call. + The SDK changes its behavior according to the new value. For example, if the current tracking consent is `.pending`: + + - if changed to `.granted`, the SDK will send all current and future data to Datadog; + - if changed to `.notGranted`, the SDK will wipe all current data and will not collect any future data. + When writing your application, you can enable development logs. All internal messages in the SDK with a priority equal to or higher than the provided level are then logged to console logs. ```swift diff --git a/docs/rum_collection.md b/docs/rum_collection.md index f8ac098405..539a877b18 100644 --- a/docs/rum_collection.md +++ b/docs/rum_collection.md @@ -51,6 +51,7 @@ github "DataDog/dd-sdk-ios" ```swift Datadog.initialize( appContext: .init(), + trackingConsent: trackingConsent, configuration: Datadog.Configuration .builderUsing( rumApplicationID: "", @@ -68,6 +69,7 @@ Datadog.initialize( ```swift Datadog.initialize( appContext: .init(), + trackingConsent: trackingConsent, configuration: Datadog.Configuration .builderUsing( rumApplicationID: "", @@ -83,6 +85,19 @@ Datadog.initialize( {{% /tab %}} {{< /tabs >}} + To be compliant with the GDPR regulation, the SDK requires the `trackingConsent` value at initialization. + The `trackingConsent` can be one of the following values: + + - `.pending` - the SDK starts collecting and batching the data but does not send it to Datadog. The SDK waits for the new tracking consent value to decide what to do with the batched data. + - `.granted` - the SDK starts collecting the data and sends it to Datadog. + - `.notGranted` - the SDK does not collect any data: logs, traces, and RUM events are not sent to Datadog. + + To change the tracking consent value after the SDK is initialized, use the `Datadog.set(trackingConsent:)` API call. + The SDK changes its behavior according to the new value. For example, if the current tracking consent is `.pending`: + + - if changed to `.granted`, the SDK will send all current and future data to Datadog; + - if changed to `.notGranted`, the SDK will wipe all current data and will not collect any future data. + 3. Configure and register the RUM Monitor. You only need to do it once, usually in your `AppDelegate` code: ```swift @@ -265,6 +280,51 @@ Global.rum.addError(message: "error message.") For more details and available options, refer to the code documentation comments in `DDRUMMonitor` class. +## Data scrubbing + +To modify the attributes of a RUM event before it is sent to Datadog or to drop an event entirely, use the event mappers API when configuring the SDK: +```swift +Datadog.Configuration + .builderUsing(...) + .setRUMViewEventMapper { viewEvent in + return viewEvent + } + .setRUMErrorEventMapper { errorEvent in + return errorEvent + } + .setRUMResourceEventMapper { resourceEvent in + return resourceEvent + } + .setRUMActionEventMapper { actionEvent in + return actionEvent + } + .build() +``` +Each mapper is a Swift closure with a signature of `(T) -> T?`, where `T` is a concrete RUM event type. This allows changing portions of the event before it gets sent. For example to redact sensitive information in RUM Resource's `url` you may implement a custom `redacted(_:) -> String` function and use it in `RUMResourceEventMapper`: +```swift +.setRUMResourceEventMapper { resourceEvent in + var resourceEvent = resourceEvent + resourceEvent.resource.url = redacted(resourceEvent.resource.url) + return resourceEvent +} +``` +Returning `nil` from the error, resource or action mapper will drop the event entirely (it won't be sent to Datadog). The value returned from the view event mapper must be not `nil`. + +Depending on a given event's type, only some specific properties can be mutated: + +| Event Type | Attribute key | Description | +|-------------------|-----------------------------------|-------------------------------------------------| +| RUMViewEvent | `viewEvent.view.name` | Name of the view | +| | `viewEvent.view.url` | URL of the view | +| RUMActionEvent | `actionEvent.action.target?.name` | Name of the action | +| | `actionEvent.view.url` | URL of the view linked to this action | +| RUMErrorEvent | `errorEvent.error.message` | Error message | +| | `errorEvent.error.stack` | Stacktrace of the error | +| | `errorEvent.error.resource?.url` | URL of the resource the error refers to | +| | `errorEvent.view.url` | URL of the view linked to this error | +| RUMResourceEvent | `resourceEvent.resource.url` | URL of the resource | +| | `resourceEvent.view.url` | URL of the view linked to this resource | + [1]: https://docs.datadoghq.com/real_user_monitoring/data_collected/ [2]: https://github.com/DataDog/dd-sdk-ios [3]: https://github.com/DataDog/dd-sdk-ios/releases diff --git a/docs/trace_collection.md b/docs/trace_collection.md index ccdef2d3dd..1a79353de6 100644 --- a/docs/trace_collection.md +++ b/docs/trace_collection.md @@ -50,6 +50,7 @@ github "DataDog/dd-sdk-ios" ```swift Datadog.initialize( appContext: .init(), + trackingConsent: trackingConsent, configuration: Datadog.Configuration .builderUsing(clientToken: "", environment: "") .set(serviceName: "app-name") @@ -63,6 +64,7 @@ Datadog.initialize( ```swift Datadog.initialize( appContext: .init(), + trackingConsent: trackingConsent, configuration: Datadog.Configuration .builderUsing(clientToken: "", environment: "") .set(serviceName: "app-name") @@ -74,7 +76,20 @@ Datadog.initialize( {{% /tab %}} {{< /tabs >}} - When writing your application, you can enable development logs. All internal messages in the SDK with a priority equal to or higher than the provided level are then logged to console logs. + To be compliant with the GDPR regulation, the SDK requires the `trackingConsent` value at initialization. + The `trackingConsent` can be one of the following values: + + - `.pending` - the SDK starts collecting and batching the data but does not send it to Datadog. The SDK waits for the new tracking consent value to decide what to do with the batched data. + - `.granted` - the SDK starts collecting the data and sends it to Datadog. + - `.notGranted` - the SDK does not collect any data: logs, traces, and RUM events are not sent to Datadog. + + To change the tracking consent value after the SDK is initialized, use the `Datadog.set(trackingConsent:)` API call. + The SDK changes its behavior according to the new value. For example, if the current tracking consent is `.pending`: + + - if changed to `.granted`, the SDK will send all current and future data to Datadog; + - if changed to `.notGranted`, the SDK will wipe all current data and will not collect any future data. + + When writing your application, you can enable development logs. All internal messages in the SDK with a priority equal to or higher than the provided level are then logged to console logs. ```swift Datadog.verbosityLevel = .debug diff --git a/tools/dogfooding/dogfood.py b/tools/dogfooding/dogfood.py new file mode 100755 index 0000000000..d94dbf457b --- /dev/null +++ b/tools/dogfooding/dogfood.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. +# This product includes software developed at Datadog (https://www.datadoghq.com/). +# Copyright 2019-Present Datadog, Inc + +import re +import subprocess +import sys +import os +import subprocess +from argparse import ArgumentParser, Namespace +from tempfile import TemporaryDirectory +from typing import Tuple + +import requests +from git import Repo + +TARGET_APP = "app" +TARGET_DEMO = "demo" + +REPOSITORIES = {TARGET_APP: "datadog-ios", TARGET_DEMO: "shopist-ios"} + + +def parse_arguments(args: list) -> Namespace: + parser = ArgumentParser() + + parser.add_argument("-v", "--version", required=True, help="the version of the SDK") + parser.add_argument("-t", "--target", required=True, + choices=[TARGET_APP, TARGET_DEMO], + help="the target repository") + + return parser.parse_args(args) + + +def github_create_pr(repository: str, branch_name: str, base_name: str, version: str, gh_token: str) -> int: + headers = { + 'authorization': "Bearer " + gh_token, + 'Accept': 'application/vnd.github.v3+json', + } + data = '{"body": "This PR has been created automatically by the CI", ' \ + '"title": "Update to version ' + version + '", ' \ + '"base":"' + base_name + '", "head":"' + branch_name + '"}' + + url = "https://api.github.com/repos/DataDog/" + repository + "/pulls" + response = requests.post(url=url, headers=headers, data=data) + if response.status_code == 201: + print("βœ” Pull Request created successfully") + return 0 + else: + print("✘ pull request failed " + str(response.status_code) + '\n' + response.text) + return 1 + + +def generate_target_code(target: str, temp_dir_path: str, version: str) -> int: + print("… Generating code with version " + version) + + if target == TARGET_APP: + print("… Updating app's Podfile") + target_file_path = os.path.join(temp_dir_path, "Podfile") + content = "" + with open(target_file_path, 'r') as target_file: + lines = target_file.readlines() + for line in lines: + if "pod 'DatadogSDK'" in line: + content = content + " pod 'DatadogSDK', :git => 'https://github.com/DataDog/dd-sdk-ios.git', :tag => '" + version + "'\n" + else: + content = content + line + + with open(target_file_path, 'w') as target_file: + target_file.write(content) + + + print("… Running `bundle exec pod install`") + os.chdir(temp_dir_path) + cmd_args = ['bundle', 'exec', 'pod', 'install'] + process = subprocess.Popen(cmd_args, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) + try: + output, errlog = process.communicate(timeout=120) + except subprocess.TimeoutExpired: + print("✘ generation timeout for " + target + ", version: " + version) + return 1 + + if process.returncode is None: + print("✘ generation status unknown for " + target + ", version: " + version) + return 1 + elif process.returncode > 0: + print("✘ generation failed for " + target + ", version: " + version) + print(output.decode("utf-8")) + print(errlog.decode("utf-8")) + return 1 + else: + return 0 + # TODO RUMM-1063 elif target == TARGET_DEMO: … + else: + print("? unknown generation target: " + target + ", version: " + version) + + +def git_clone_repository(repo_name: str, gh_token: str, temp_dir_path: str) -> Tuple[Repo, str]: + print("… Cloning repository " + repo_name) + url = "https://" + gh_token + ":x-oauth-basic@github.com/DataDog/" + repo_name + repo = Repo.clone_from(url, temp_dir_path) + base_name = repo.active_branch.name + return repo, base_name + + +def git_push_changes(repo: Repo, version: str): + print("… Committing changes") + repo.git.add(update=True) + repo.index.commit("Update DD SDK to " + version) + + print("β‘Š Pushing branch") + origin = repo.remote(name="origin") + repo.git.push("--set-upstream", "--force", origin, repo.head.ref) + + +def update_dependant(version: str, target: str, gh_token: str) -> int: + branch_name = "update_sdk_" + version + temp_dir = TemporaryDirectory() + temp_dir_path = temp_dir.name + repo_name = REPOSITORIES[target] + + repo, base_name = git_clone_repository(repo_name, gh_token, temp_dir_path) + + print("… Creating branch " + branch_name) + repo.git.checkout('HEAD', b=branch_name) + + + cwd = os.getcwd() + result = generate_target_code(target, temp_dir_path, version) + os.chdir(cwd) + + if result > 0: + return result + + if not repo.is_dirty(): + print("βˆ… Nothing to commit, all is in order…") + return 0 + + git_push_changes(repo, version) + + return github_create_pr(repo_name, branch_name, base_name, version, gh_token) + +def run_main() -> int: + cli_args = parse_arguments(sys.argv[1:]) + + if cli_args.target != TARGET_APP: + print("Cannot dogfood target : " + cli_args.target) + return 1 + + # This script expects to have a valid Github Token in a "gh_token" text file + # The token needs the `repo` permissions, and for now is a PAT + with open('gh_token', 'r') as f: + gh_token = f.read().strip() + + return update_dependant(cli_args.version, cli_args.target, gh_token) + + +if __name__ == "__main__": + sys.exit(run_main()) diff --git a/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Input/JSON/JSONSchema.swift b/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Input/JSON/JSONSchema.swift index 1ed34f31d8..d1cbd98c38 100644 --- a/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Input/JSON/JSONSchema.swift +++ b/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Input/JSON/JSONSchema.swift @@ -16,6 +16,7 @@ internal class JSONSchema: Decodable { case title = "title" case description = "description" case properties = "properties" + case additionalProperties = "additionalProperties" case required = "required" case type = "type" case `enum` = "enum" @@ -65,6 +66,7 @@ internal class JSONSchema: Decodable { private(set) var title: String? private(set) var description: String? private(set) var properties: [String: JSONSchema]? + private(set) var additionalProperties: JSONSchema? private(set) var required: [String]? private(set) var type: SchemaType? private(set) var `enum`: [String]? @@ -143,6 +145,8 @@ internal class JSONSchema: Decodable { self.properties = self.properties ?? otherSchema.properties } + self.additionalProperties = self.additionalProperties ?? otherSchema.additionalProperties + // Required properties are accumulated. if let selfRequired = self.required, let otherRequired = otherSchema.required { self.required = selfRequired + otherRequired diff --git a/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Input/JSON/JSONSchemaToJSONTypeTransformer.swift b/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Input/JSON/JSONSchemaToJSONTypeTransformer.swift index b390bd654a..ce51dd93eb 100644 --- a/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Input/JSON/JSONSchemaToJSONTypeTransformer.swift +++ b/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Input/JSON/JSONSchemaToJSONTypeTransformer.swift @@ -99,7 +99,7 @@ internal class JSONSchemaToJSONTypeTransformer { name: propertyName, comment: propertySchema.description, type: try transformSchemaToAnyType(propertySchema, named: propertyName), - defaultVaule: propertySchema.const.flatMap { const in + defaultValue: propertySchema.const.flatMap { const in switch const.value { case .integer(let value): return .integer(value: value) case .string(let value): return .string(value: value) @@ -108,10 +108,26 @@ internal class JSONSchemaToJSONTypeTransformer { isRequired: schema.required?.contains(propertyName) ?? Defaults.isRequired, isReadOnly: propertySchema.readOnly ?? Defaults.isReadOnly ) - properties.append(property) } - return JSONObject(name: name, comment: schema.description, properties: properties) + let additionalProperties: JSONObject.AdditionalProperties? + if let additionalPropertiesSchema = schema.additionalProperties { + let type = try transformSchemaToPrimitive(additionalPropertiesSchema) + additionalProperties = JSONObject.AdditionalProperties( + comment: additionalPropertiesSchema.description, + type: type, + isReadOnly: additionalPropertiesSchema.readOnly ?? Defaults.isReadOnly + ) + } else { + additionalProperties = nil + } + + return JSONObject( + name: name, + comment: schema.description, + properties: properties, + additionalProperties: additionalProperties + ) } } diff --git a/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Input/JSON/JSONType.swift b/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Input/JSON/JSONType.swift index af75d08d5c..9648cd00e4 100644 --- a/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Input/JSON/JSONType.swift +++ b/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Input/JSON/JSONType.swift @@ -36,19 +36,27 @@ internal struct JSONObject: JSONType { let name: String let comment: String? let type: JSONType - let defaultVaule: DefaultValue? + let defaultValue: DefaultValue? let isRequired: Bool let isReadOnly: Bool } + struct AdditionalProperties: JSONType { + let comment: String? + let type: JSONPrimitive + let isReadOnly: Bool + } + let name: String let comment: String? let properties: [Property] + let additionalProperties: AdditionalProperties? - init(name: String, comment: String?, properties: [Property]) { + init(name: String, comment: String?, properties: [Property], additionalProperties: AdditionalProperties? = nil) { self.name = name self.comment = comment self.properties = properties.sorted { property1, property2 in property1.name < property2.name } + self.additionalProperties = additionalProperties } } diff --git a/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Input/ObjcInterop/ObjcInteropType.swift b/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Input/ObjcInterop/ObjcInteropType.swift index 7ae271a2c0..a29365bf0e 100644 --- a/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Input/ObjcInterop/ObjcInteropType.swift +++ b/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Input/ObjcInterop/ObjcInteropType.swift @@ -19,11 +19,11 @@ internal protocol ObjcInteropClass: ObjcInteropType { internal class ObjcInteropRootClass: ObjcInteropClass { /// The `SwiftStruct` managed by this `@objc class`. let bridgedSwiftStruct: SwiftStruct - /// `managedSwiftStruct's` property wrappers exposed to Objc. + /// `bridgedSwiftStruct's` property wrappers exposed to Objc. var objcPropertyWrappers: [ObjcInteropPropertyWrapper] = [] - init(managedSwiftStruct: SwiftStruct) { - self.bridgedSwiftStruct = managedSwiftStruct + init(bridgedSwiftStruct: SwiftStruct) { + self.bridgedSwiftStruct = bridgedSwiftStruct } } @@ -34,12 +34,12 @@ internal class ObjcInteropTransitiveClass: ObjcInteropClass { /// The nested `SwiftStruct` managed by this `@objc class`. let bridgedSwiftStruct: SwiftStruct - /// `managedSwiftStruct's` property wrappers exposed to Objc. + /// `bridgedSwiftStruct's` property wrappers exposed to Objc. var objcPropertyWrappers: [ObjcInteropPropertyWrapper] = [] - init(owner: ObjcInteropPropertyWrapper, managedSwiftStruct: SwiftStruct) { + init(owner: ObjcInteropPropertyWrapper, bridgedSwiftStruct: SwiftStruct) { self.parentProperty = owner - self.bridgedSwiftStruct = managedSwiftStruct + self.bridgedSwiftStruct = bridgedSwiftStruct } } @@ -53,9 +53,9 @@ internal class ObjcInteropEnum: ObjcInteropType { /// The `SwiftEnum` exposed by this Obj-c enum. let bridgedSwiftEnum: SwiftEnum - init(owner: ObjcInteropPropertyWrapper, managedSwiftEnum: SwiftEnum) { + init(owner: ObjcInteropPropertyWrapper, bridgedSwiftEnum: SwiftEnum) { self.parentProperty = owner - self.bridgedSwiftEnum = managedSwiftEnum + self.bridgedSwiftEnum = bridgedSwiftEnum } } @@ -133,3 +133,13 @@ internal class ObjcInteropNSArray: ObjcInteropType { self.element = element } } + +internal class ObjcInteropNSDictionary: ObjcInteropType { + let key: ObjcInteropType + let value: ObjcInteropType + + init(key: ObjcInteropType, value: ObjcInteropType) { + self.key = key + self.value = value + } +} diff --git a/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Input/ObjcInterop/SwiftToObjcInteropTypeTransformer.swift b/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Input/ObjcInterop/SwiftToObjcInteropTypeTransformer.swift index 90417fa78e..356f3cbf67 100644 --- a/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Input/ObjcInterop/SwiftToObjcInteropTypeTransformer.swift +++ b/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Input/ObjcInterop/SwiftToObjcInteropTypeTransformer.swift @@ -19,7 +19,7 @@ internal class SwiftToObjcInteropTypeTransformer { try takeRootSwiftStructs(from: swiftTypes) .forEach { rootStruct in - let rootClass = ObjcInteropRootClass(managedSwiftStruct: rootStruct) + let rootClass = ObjcInteropRootClass(bridgedSwiftStruct: rootStruct) outputObjcInteropTypes.append(rootClass) try generateTransitiveObjcInteropTypes(in: rootClass) } @@ -48,7 +48,7 @@ internal class SwiftToObjcInteropTypeTransformer { ) propertyWrapper.objcNestedClass = ObjcInteropTransitiveClass( owner: propertyWrapper, - managedSwiftStruct: swiftStruct + bridgedSwiftStruct: swiftStruct ) return propertyWrapper case let swiftEnum as SwiftEnum: @@ -58,7 +58,7 @@ internal class SwiftToObjcInteropTypeTransformer { ) propertyWrapper.objcNestedEnum = ObjcInteropEnum( owner: propertyWrapper, - managedSwiftEnum: swiftEnum + bridgedSwiftEnum: swiftEnum ) return propertyWrapper case let swiftArray as SwiftArray where swiftArray.element is SwiftEnum: @@ -68,7 +68,7 @@ internal class SwiftToObjcInteropTypeTransformer { ) propertyWrapper.objcNestedEnumsArray = ObjcInteropEnumArray( owner: propertyWrapper, - managedSwiftEnum: swiftArray.element as! SwiftEnum // swiftlint:disable:this force_cast + bridgedSwiftEnum: swiftArray.element as! SwiftEnum // swiftlint:disable:this force_cast ) return propertyWrapper case let swiftArray as SwiftArray where swiftArray.element is SwiftPrimitiveType: @@ -78,6 +78,13 @@ internal class SwiftToObjcInteropTypeTransformer { ) propertyWrapper.objcInteropType = try objcInteropType(for: swiftArray) return propertyWrapper + case let swiftDictionary as SwiftDictionary: + let propertyWrapper = ObjcInteropPropertyWrapperManagingSwiftStructProperty( + owner: objcClass, + swiftProperty: swiftProperty + ) + propertyWrapper.objcInteropType = try objcInteropType(for: swiftDictionary) + return propertyWrapper case let swifTypeReference as SwiftTypeReference: let referencedType = try resolve(swiftTypeReference: swifTypeReference) @@ -89,7 +96,7 @@ internal class SwiftToObjcInteropTypeTransformer { ) propertyWrapper.objcNestedClass = ObjcInteropReferencedTransitiveClass( owner: propertyWrapper, - managedSwiftStruct: swiftStruct + bridgedSwiftStruct: swiftStruct ) return propertyWrapper case let swiftEnum as SwiftEnum: @@ -99,7 +106,7 @@ internal class SwiftToObjcInteropTypeTransformer { ) propertyWrapper.objcNestedEnum = ObjcInteropReferencedEnum( owner: propertyWrapper, - managedSwiftEnum: swiftEnum + bridgedSwiftEnum: swiftEnum ) return propertyWrapper default: @@ -135,6 +142,11 @@ internal class SwiftToObjcInteropTypeTransformer { return ObjcInteropNSString(swiftString: swiftString) case let swiftArray as SwiftArray: return ObjcInteropNSArray(element: try objcInteropType(for: swiftArray.element)) + case let swiftDictionary as SwiftDictionary: + return ObjcInteropNSDictionary( + key: try objcInteropType(for: swiftDictionary.key), + value: try objcInteropType(for: swiftDictionary.value) + ) default: throw Exception.unimplemented( "Cannot create `ObjcInteropType` type for \(type(of: swiftType))." diff --git a/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Input/Swift/JSONToSwiftTypeTransformer.swift b/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Input/Swift/JSONToSwiftTypeTransformer.swift index ec81c7d594..3f7579537b 100644 --- a/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Input/Swift/JSONToSwiftTypeTransformer.swift +++ b/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Input/Swift/JSONToSwiftTypeTransformer.swift @@ -13,6 +13,9 @@ internal class JSONToSwiftTypeTransformer { } private func transform(jsonObject: JSONObject) throws -> SwiftStruct { + if jsonObject.additionalProperties != nil { + throw Exception.unimplemented("Transforming root object \(jsonObject) with `additionalProperties` is not supported.") + } var `struct` = try transformJSONToStruct(jsonObject) `struct` = resolveTransitiveMutableProperties(in: `struct`) return `struct` @@ -29,7 +32,7 @@ internal class JSONToSwiftTypeTransformer { case let jsonEnumeration as JSONEnumeration: return transformJSONToEnum(jsonEnumeration) case let jsonObject as JSONObject: - return try transformJSONToStruct(jsonObject) + return try transformJSONObject(jsonObject) default: throw Exception.unimplemented("Transforming \(json) into `SwiftType` is not supported.") } @@ -61,12 +64,25 @@ internal class JSONToSwiftTypeTransformer { ) } + private func transformJSONObject(_ jsonObject: JSONObject) throws -> SwiftType { + if let additionalProperties = jsonObject.additionalProperties { + if jsonObject.properties.count > 0 { + throw Exception.unimplemented("Transforming \(jsonObject) with both `properties` and `additionalProperties` is not supported.") + } + return SwiftDictionary( + value: transformJSONtoPrimitive(additionalProperties.type) + ) + } else { + return try transformJSONToStruct(jsonObject) + } + } + private func transformJSONToStruct(_ jsonObject: JSONObject) throws -> SwiftStruct { /// Reads Struct properties. func readProperties(from objectProperties: [JSONObject.Property]) throws -> [SwiftStruct.Property] { /// Reads Struct property default value. func readDefaultValue(for objectProperty: JSONObject.Property) throws -> SwiftPropertyDefaultValue? { - return objectProperty.defaultVaule.ifNotNil { value in + return objectProperty.defaultValue.ifNotNil { value in switch value { case .integer(let intValue): return intValue @@ -87,7 +103,7 @@ internal class JSONToSwiftTypeTransformer { type: try transformJSONToAnyType(jsonProperty.type), isOptional: !jsonProperty.isRequired, isMutable: !jsonProperty.isReadOnly, - defaultVaule: try readDefaultValue(for: jsonProperty), + defaultValue: try readDefaultValue(for: jsonProperty), codingKey: jsonProperty.name ) } diff --git a/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Input/Swift/SwiftType.swift b/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Input/Swift/SwiftType.swift index 36d09117b8..183284080d 100644 --- a/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Input/Swift/SwiftType.swift +++ b/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Input/Swift/SwiftType.swift @@ -28,6 +28,11 @@ internal struct SwiftArray: SwiftType { var element: SwiftType } +internal struct SwiftDictionary: SwiftType { + let key = SwiftPrimitive() + var value: SwiftPrimitiveType +} + internal struct SwiftEnum: SwiftType { struct Case: SwiftType, SwiftPropertyDefaultValue { var label: String @@ -47,7 +52,7 @@ internal struct SwiftStruct: SwiftType { var type: SwiftType var isOptional: Bool var isMutable: Bool - var defaultVaule: SwiftPropertyDefaultValue? + var defaultValue: SwiftPropertyDefaultValue? var codingKey: String } diff --git a/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Output/Printing/ObjcInteropPrinter.swift b/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Output/Printing/ObjcInteropPrinter.swift index b958f1f118..504f7838f5 100644 --- a/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Output/Printing/ObjcInteropPrinter.swift +++ b/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Output/Printing/ObjcInteropPrinter.swift @@ -310,6 +310,8 @@ internal class ObjcInteropPrinter: BasePrinter { return "String" case let objcArray as ObjcInteropNSArray: return "[\(try objcInteropTypeName(for: objcArray.element))]" + case let objcDictionary as ObjcInteropNSDictionary: + return "[\(try objcInteropTypeName(for: objcDictionary.key)): \(try objcInteropTypeName(for: objcDictionary.value))]" default: throw Exception.unimplemented( "Cannot print `ObjcInteropType` name for \(type(of: objcType))." @@ -323,10 +325,14 @@ internal class ObjcInteropPrinter: BasePrinter { return " as NSNumber" case let nsArray as ObjcInteropNSArray where nsArray.element is ObjcInteropNSNumber: return " as [NSNumber]" + case let nsDictionary as ObjcInteropNSDictionary where nsDictionary.value is ObjcInteropNSNumber: + return " as [\(try objcInteropTypeName(for: nsDictionary.key)): NSNumber]" case _ as ObjcInteropNSString: return nil // `String` <> `NSString` interoperability doesn't require casting case let nsArray as ObjcInteropNSArray where nsArray.element is ObjcInteropNSString: return nil // `[String]` <> `[NSString]` interoperability doesn't require casting + case let nsDictionary as ObjcInteropNSDictionary where nsDictionary.value is ObjcInteropNSString: + return nil // `[Key: String]` <> `[Key: NSString]` interoperability doesn't require casting default: throw Exception.unimplemented("Cannot print `swiftToObjcCast()` for \(type(of: objcType)).") } @@ -344,10 +350,17 @@ internal class ObjcInteropPrinter: BasePrinter { return ".int64Value" case let swiftArray as SwiftArray where swiftArray.element is SwiftPrimitive: return nil // `[String]` <> `[NSString]` interoperability doesn't require casting + case let swiftDictionary as SwiftDictionary where swiftDictionary.value is SwiftPrimitive: + return nil // `[Key: String]` <> `[Key: NSString]` interoperability doesn't require casting case let swiftArray as SwiftArray: let elementCast = try objcToSwiftCast(for: swiftArray.element) .unwrapOrThrow(.illegal("Cannot print `objcToSwiftCast()` for `SwiftArray` with elements of type: \(type(of: swiftArray.element))")) return ".map { $0\(elementCast) }" + case let swiftDictionary as SwiftDictionary: + let keyCast = try objcToSwiftCast(for: swiftDictionary.key) ?? "" + let valueCast = try objcToSwiftCast(for: swiftDictionary.value) + .unwrapOrThrow(.illegal("Cannot print `objcToSwiftCast()` for `SwiftDictionary` with values of type: \(type(of: swiftDictionary.value))")) + return ".reduce(into: [:]) { $0[$1.0\(keyCast)] = $1.1\(valueCast)" case _ as SwiftPrimitive: return nil // `String` <> `NSString` interoperability doesn't require casting default: diff --git a/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Output/Printing/SwiftPrinter.swift b/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Output/Printing/SwiftPrinter.swift index 9c4e454c9e..e4de4a81e2 100644 --- a/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Output/Printing/SwiftPrinter.swift +++ b/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/Output/Printing/SwiftPrinter.swift @@ -56,7 +56,7 @@ public class SwiftPrinter: BasePrinter { let name = property.name let type = try typeDeclaration(property.type) let optionality = property.isOptional ? "?" : "" - let defaultValue: String? = try property.defaultVaule.ifNotNil { value in + let defaultValue: String? = try property.defaultValue.ifNotNil { value in switch value { case let integerValue as Int: return " = \(integerValue)" @@ -135,6 +135,8 @@ public class SwiftPrinter: BasePrinter { return "String" case let swiftArray as SwiftArray: return "[\(try typeDeclaration(swiftArray.element))]" + case let swiftDictionary as SwiftDictionary: + return "[\(try typeDeclaration(swiftDictionary.key)): \(try typeDeclaration(swiftDictionary.value))]" case let swiftEnum as SwiftEnum: return swiftEnum.name case let swiftStruct as SwiftStruct: diff --git a/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/RUM/RUMSwiftTypeTransformer.swift b/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/RUM/RUMSwiftTypeTransformer.swift index 01383d4948..62a1a83c3a 100644 --- a/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/RUM/RUMSwiftTypeTransformer.swift +++ b/tools/rum-models-generator/Sources/RUMModelsGeneratorCore/RUM/RUMSwiftTypeTransformer.swift @@ -35,6 +35,8 @@ internal class RUMSwiftTypeTransformer: TypeTransformer { return transform(primitive: primitive) case let array as SwiftArray: return try transform(array: array) + case let dictionary as SwiftDictionary: + return transform(dictionary: dictionary) case let `enum` as SwiftEnum: let transformed = transform(enum: `enum`) return isSharedType(transformed) ? try replaceWithSharedTypeReference(transformed) : transformed @@ -54,6 +56,12 @@ internal class RUMSwiftTypeTransformer: TypeTransformer { } } + private func transform(dictionary: SwiftDictionary) -> SwiftDictionary { + var dictionary = dictionary + dictionary.value = transform(primitive: dictionary.value) + return dictionary + } + private func transform(array: SwiftArray) throws -> SwiftArray { var array = array array.element = try transformAny(type: array.element) @@ -88,7 +96,7 @@ internal class RUMSwiftTypeTransformer: TypeTransformer { var structProperty = structProperty structProperty.name = format(propertyName: structProperty.name) structProperty.type = try transformAny(type: structProperty.type) - structProperty.defaultVaule = structProperty.defaultVaule.ifNotNil { transform(defaultValue: $0) } + structProperty.defaultValue = structProperty.defaultValue.ifNotNil { transform(defaultValue: $0) } return structProperty } @@ -96,8 +104,6 @@ internal class RUMSwiftTypeTransformer: TypeTransformer { `struct`.name = format(structName: `struct`.name) `struct`.properties = try `struct`.properties .map { try transform(structProperty: $0) } - // TODO: RUMM-1000 should remove this filter - .filter { property in property.name != "customTimings" } if context.parent == nil { `struct`.conformance = [rumDataModelProtocol] // Conform root structs to `RUMDataModel` } else { diff --git a/tools/rum-models-generator/Tests/rum-models-generator-coreTests/Input/JSONSchemaToJSONTypeTransformerTests.swift b/tools/rum-models-generator/Tests/rum-models-generator-coreTests/Input/JSONSchemaToJSONTypeTransformerTests.swift index 49f7d01070..8a25b7fc1d 100644 --- a/tools/rum-models-generator/Tests/rum-models-generator-coreTests/Input/JSONSchemaToJSONTypeTransformerTests.swift +++ b/tools/rum-models-generator/Tests/rum-models-generator-coreTests/Input/JSONSchemaToJSONTypeTransformerTests.swift @@ -74,9 +74,24 @@ final class JSONSchemaToJSONTypeTransformerTests: XCTestCase { "enum": ["option1", "option2", "option3", "option4"] }, "readOnly": false + }, + "propertyWithAdditionalProperties": { + "type": "object", + "description": "Description of a property with nested additional properties.", + "additionalProperties": { + "type": "integer", + "minimum": 0, + "readOnly": true + }, + "readOnly": true } }, - "required": ["property1"] + "additionalProperties": { + "type": "string", + "description": "Additional properties of Foo.", + "readOnly": true + }, + "required": ["property1"], } ] } @@ -97,7 +112,7 @@ final class JSONSchemaToJSONTypeTransformerTests: XCTestCase { name: "property1", comment: "Description of Bar's `property1`.", type: JSONPrimitive.string, - defaultVaule: nil, + defaultValue: nil, isRequired: false, isReadOnly: true ), @@ -105,13 +120,13 @@ final class JSONSchemaToJSONTypeTransformerTests: XCTestCase { name: "property2", comment: "Description of Bar's `property2`.", type: JSONPrimitive.string, - defaultVaule: nil, + defaultValue: nil, isRequired: true, isReadOnly: false ) ] ), - defaultVaule: nil, + defaultValue: nil, isRequired: false, isReadOnly: true ), @@ -123,7 +138,7 @@ final class JSONSchemaToJSONTypeTransformerTests: XCTestCase { comment: "Description of Foo's `property1`.", values: ["case1", "case2", "case3", "case4"] ), - defaultVaule: JSONObject.Property.DefaultValue.string(value: "case2"), + defaultValue: JSONObject.Property.DefaultValue.string(value: "case2"), isRequired: true, isReadOnly: true ), @@ -137,11 +152,34 @@ final class JSONSchemaToJSONTypeTransformerTests: XCTestCase { values: ["option1", "option2", "option3", "option4"] ) ), - defaultVaule: nil, + defaultValue: nil, isRequired: false, isReadOnly: false + ), + JSONObject.Property( + name: "propertyWithAdditionalProperties", + comment: "Description of a property with nested additional properties.", + type: JSONObject( + name: "propertyWithAdditionalProperties", + comment: "Description of a property with nested additional properties.", + properties: [], + additionalProperties: + JSONObject.AdditionalProperties( + comment: nil, + type: JSONPrimitive.integer, + isReadOnly: true + ) + ), + defaultValue: nil, + isRequired: false, + isReadOnly: true ) - ] + ], + additionalProperties: JSONObject.AdditionalProperties( + comment: "Additional properties of Foo.", + type: JSONPrimitive.string, + isReadOnly: true + ) ) let jsonSchema = try JSONSchemaReader() diff --git a/tools/rum-models-generator/Tests/rum-models-generator-coreTests/Input/JSONToSwiftTypeTransformerTests.swift b/tools/rum-models-generator/Tests/rum-models-generator-coreTests/Input/JSONToSwiftTypeTransformerTests.swift index 2428627d35..f0d1e83473 100644 --- a/tools/rum-models-generator/Tests/rum-models-generator-coreTests/Input/JSONToSwiftTypeTransformerTests.swift +++ b/tools/rum-models-generator/Tests/rum-models-generator-coreTests/Input/JSONToSwiftTypeTransformerTests.swift @@ -24,7 +24,7 @@ final class JSONToSwiftTypeTransformerTests: XCTestCase { name: "property1", comment: "Description of Bar's `property1`.", type: JSONPrimitive.string, - defaultVaule: nil, + defaultValue: nil, isRequired: false, isReadOnly: true ), @@ -32,13 +32,13 @@ final class JSONToSwiftTypeTransformerTests: XCTestCase { name: "property2", comment: "Description of Bar's `property2`.", type: JSONPrimitive.string, - defaultVaule: nil, + defaultValue: nil, isRequired: true, isReadOnly: false ) ] ), - defaultVaule: nil, + defaultValue: nil, isRequired: false, isReadOnly: true ), @@ -50,7 +50,7 @@ final class JSONToSwiftTypeTransformerTests: XCTestCase { comment: "Description of Foo's `property1`.", values: ["case1", "case2", "case3", "case4"] ), - defaultVaule: JSONObject.Property.DefaultValue.string(value: "case2"), + defaultValue: JSONObject.Property.DefaultValue.string(value: "case2"), isRequired: true, isReadOnly: true ), @@ -64,7 +64,7 @@ final class JSONToSwiftTypeTransformerTests: XCTestCase { values: ["option1", "option2", "option3", "option4"] ) ), - defaultVaule: nil, + defaultValue: nil, isRequired: false, isReadOnly: false ) @@ -88,7 +88,7 @@ final class JSONToSwiftTypeTransformerTests: XCTestCase { type: SwiftPrimitive(), isOptional: true, isMutable: false, - defaultVaule: nil, + defaultValue: nil, codingKey: "property1" ), SwiftStruct.Property( @@ -97,7 +97,7 @@ final class JSONToSwiftTypeTransformerTests: XCTestCase { type: SwiftPrimitive(), isOptional: false, isMutable: true, - defaultVaule: nil, + defaultValue: nil, codingKey: "property2" ) ], @@ -105,7 +105,7 @@ final class JSONToSwiftTypeTransformerTests: XCTestCase { ), isOptional: true, isMutable: true, // should be mutable as at least one of the `Bar's` properties is mutable - defaultVaule: nil, + defaultValue: nil, codingKey: "bar" ), SwiftStruct.Property( @@ -124,7 +124,7 @@ final class JSONToSwiftTypeTransformerTests: XCTestCase { ), isOptional: false, isMutable: false, - defaultVaule: SwiftEnum.Case(label: "case2", rawValue: "case2"), + defaultValue: SwiftEnum.Case(label: "case2", rawValue: "case2"), codingKey: "property1" ), SwiftStruct.Property( @@ -145,7 +145,7 @@ final class JSONToSwiftTypeTransformerTests: XCTestCase { ), isOptional: true, isMutable: true, - defaultVaule: nil, + defaultValue: nil, codingKey: "property2" ) ], @@ -157,4 +157,184 @@ final class JSONToSwiftTypeTransformerTests: XCTestCase { XCTAssertEqual(actual.count, 1) XCTAssertEqual(expected, actual[0]) } + + func testTransformingNestedJSONObjectWithAdditionalPropertiesIntoSwiftDictionaryInsideRootStruct() throws { + let object = JSONObject( + name: "Foo", + comment: "Description of Foo.", + properties: [ + JSONObject.Property( + name: "propertyWithAdditionalProperties", + comment: "Description of a property with nested additional properties.", + type: JSONObject( + name: "propertyWithAdditionalProperties", + comment: "Description of a property with nested additional properties.", + properties: [], + additionalProperties: + JSONObject.AdditionalProperties( + comment: nil, + type: JSONPrimitive.integer, + isReadOnly: true + ) + ), + defaultValue: nil, + isRequired: false, + isReadOnly: true + ) + ] + ) + + let expected = SwiftStruct( + name: "Foo", + comment: "Description of Foo.", + properties: [ + SwiftStruct.Property( + name: "propertyWithAdditionalProperties", + comment: "Description of a property with nested additional properties.", + type: SwiftDictionary(value: SwiftPrimitive()), + isOptional: true, + isMutable: false, + defaultValue: nil, + codingKey: "propertyWithAdditionalProperties" + ) + ], + conformance: [] + ) + + let actual = try JSONToSwiftTypeTransformer().transform(jsonObjects: [object]) + + XCTAssertEqual(actual.count, 1) + XCTAssertEqual(expected, actual[0]) + } + + func testTransformingNestedJSONObjectWithPropertiesAndAdditionalPropertiesIntoSwiftStruct() throws { + let object = JSONObject( + name: "Foo", + comment: "Description of Foo.", + properties: [ + JSONObject.Property( + name: "bar", + comment: "Description of Foo's `bar`.", + type: JSONObject( + name: "bar", + comment: "Description of Foo's `baz`.", + properties: [ + JSONObject.Property( + name: "baz", + comment: "Description of Foo.bar's `baz`.", + type: JSONPrimitive.string, + defaultValue: nil, + isRequired: false, + isReadOnly: true + ) + ], + additionalProperties: JSONObject.AdditionalProperties( + comment: "Additional properties of property1.", + type: JSONPrimitive.string, + isReadOnly: true + ) + ), + defaultValue: nil, + isRequired: false, + isReadOnly: true + ) + ] + ) + + var error: Error? = nil + XCTAssertThrowsError(try JSONToSwiftTypeTransformer().transform(jsonObjects: [object])) { error = $0 } + let exception = try XCTUnwrap(error as? Exception) + XCTAssertTrue(exception.description.contains("not supported")) + } + + func testTransformingRootJSONObjectWithAdditionalPropertiesIntoSwiftStruct() throws { + let object = JSONObject( + name: "Foo", + comment: "Description of Foo.", + properties: [ + JSONObject.Property( + name: "bar", + comment: "Description of Foo's `bar`.", + type: JSONObject( + name: "bar", + comment: "Description of Foo's `baz`.", + properties: [ + JSONObject.Property( + name: "baz", + comment: "Description of Foo.bar's `baz`.", + type: JSONPrimitive.string, + defaultValue: nil, + isRequired: false, + isReadOnly: true + ) + ] + ), + defaultValue: nil, + isRequired: false, + isReadOnly: true + ) + ], + additionalProperties: JSONObject.AdditionalProperties( + comment: "Additional properties of Foo.", + type: JSONPrimitive.string, + isReadOnly: true + ) + ) + + var error: Error? = nil + XCTAssertThrowsError(try JSONToSwiftTypeTransformer().transform(jsonObjects: [object])) { error = $0 } + let exception = try XCTUnwrap(error as? Exception) + XCTAssertTrue(exception.description.contains("not supported")) + } + + func testTransformingJSONObjectPropertyWithAdditionalPropertiesAndConflictingFlags() throws { + let object = JSONObject( + name: "Foo", + comment: "Description of Foo.", + properties: [ + JSONObject.Property( + name: "propertyWithAdditionalProperties", + comment: "Description of a property with nested additional properties.", + type: JSONObject( + name: "propertyWithAdditionalProperties", + comment: "Description of a property with nested additional properties.", + properties: [], + additionalProperties: + JSONObject.AdditionalProperties( + comment: nil, + type: JSONPrimitive.integer, + isReadOnly: false + ) + ), + defaultValue: nil, + isRequired: true, // Expect this flag to be take precedence over the inner `additionalProperties`. + isReadOnly: true // Expect this flag to be take precedence over the inner `additionalProperties`. + ) + ] + ) + + let expected = SwiftStruct( + name: "Foo", + comment: "Description of Foo.", + properties: [ + SwiftStruct.Property( + name: "propertyWithAdditionalProperties", + comment: "Description of a property with nested additional properties.", + type: SwiftDictionary( + value: SwiftPrimitive() + ), + isOptional: false, + isMutable: false, + defaultValue: nil, + codingKey: "propertyWithAdditionalProperties" + ) + ], + conformance: [] + ) + + let actual = try JSONToSwiftTypeTransformer().transform(jsonObjects: [object]) + + XCTAssertEqual(actual.count, 1) + XCTAssertEqual(expected, actual[0]) + } } diff --git a/tools/rum-models-generator/Tests/rum-models-generator-coreTests/Output/ObjcInteropPrinterTests.swift b/tools/rum-models-generator/Tests/rum-models-generator-coreTests/Output/ObjcInteropPrinterTests.swift index 159a55a44e..b9903f1b13 100644 --- a/tools/rum-models-generator/Tests/rum-models-generator-coreTests/Output/ObjcInteropPrinterTests.swift +++ b/tools/rum-models-generator/Tests/rum-models-generator-coreTests/Output/ObjcInteropPrinterTests.swift @@ -958,6 +958,174 @@ final class ObjcInteropPrinterTests: XCTestCase { XCTAssertEqual(expected, actual) } + // MARK: - Property wrappers for Swift Dictionaries + + func testPrintingObjcInteropForSwiftStructWithStringDictionaryProperties() throws { + let fooStruct = SwiftStruct( + name: "Foo", + comment: nil, + properties: [ + .mock( + propertyName: "immutableStrings", + type: SwiftDictionary(value: SwiftPrimitive()), + isOptional: false, + isMutable: false + ), + .mock( + propertyName: "optionalImmutableStrings", + type: SwiftDictionary(value: SwiftPrimitive()), + isOptional: true, + isMutable: false + ), + .mock( + propertyName: "mutableStrings", + type: SwiftDictionary(value: SwiftPrimitive()), + isOptional: false, + isMutable: true + ), + .mock( + propertyName: "optionalMutableStrings", + type: SwiftDictionary(value: SwiftPrimitive()), + isOptional: true, + isMutable: true + ), + ], + conformance: [] + ) + + let expected = """ + // MARK: - Swift + + public struct Foo { + public let immutableStrings: [String: String] + + public let optionalImmutableStrings: [String: String]? + + public var mutableStrings: [String: String] + + public var optionalMutableStrings: [String: String]? + } + + // MARK: - ObjcInterop + + @objc + public class DDFoo: NSObject { + internal var swiftModel: Foo + internal var root: DDFoo { self } + + internal init(swiftModel: Foo) { + self.swiftModel = swiftModel + } + + @objc public var immutableStrings: [String: String] { + root.swiftModel.immutableStrings + } + + @objc public var optionalImmutableStrings: [String: String]? { + root.swiftModel.optionalImmutableStrings + } + + @objc public var mutableStrings: [String: String] { + set { root.swiftModel.mutableStrings = newValue } + get { root.swiftModel.mutableStrings } + } + + @objc public var optionalMutableStrings: [String: String]? { + set { root.swiftModel.optionalMutableStrings = newValue } + get { root.swiftModel.optionalMutableStrings } + } + } + + """ + + let actual = try printSwiftWithObjcInterop(for: [fooStruct]) + + XCTAssertEqual(expected, actual) + } + + func testPrintingObjcInteropForSwiftStructWithInt64DictionaryProperties() throws { + let fooStruct = SwiftStruct( + name: "Foo", + comment: nil, + properties: [ + .mock( + propertyName: "immutableInt64s", + type: SwiftDictionary(value: SwiftPrimitive()), + isOptional: false, + isMutable: false + ), + .mock( + propertyName: "optionalImmutableInt64s", + type: SwiftDictionary(value: SwiftPrimitive()), + isOptional: true, + isMutable: false + ), + .mock( + propertyName: "mutableInt64s", + type: SwiftDictionary(value: SwiftPrimitive()), + isOptional: false, + isMutable: true + ), + .mock( + propertyName: "optionalMutableInt64s", + type: SwiftDictionary(value: SwiftPrimitive()), + isOptional: true, + isMutable: true + ), + ], + conformance: [] + ) + + let expected = """ + // MARK: - Swift + + public struct Foo { + public let immutableInt64s: [String: Int64] + + public let optionalImmutableInt64s: [String: Int64]? + + public var mutableInt64s: [String: Int64] + + public var optionalMutableInt64s: [String: Int64]? + } + + // MARK: - ObjcInterop + + @objc + public class DDFoo: NSObject { + internal var swiftModel: Foo + internal var root: DDFoo { self } + + internal init(swiftModel: Foo) { + self.swiftModel = swiftModel + } + + @objc public var immutableInt64s: [String: NSNumber] { + root.swiftModel.immutableInt64s as [String: NSNumber] + } + + @objc public var optionalImmutableInt64s: [String: NSNumber]? { + root.swiftModel.optionalImmutableInt64s as [String: NSNumber]? + } + + @objc public var mutableInt64s: [String: NSNumber] { + set { root.swiftModel.mutableInt64s = newValue.reduce(into: [:]) { $0[$1.0] = $1.1.int64Value } + get { root.swiftModel.mutableInt64s as [String: NSNumber] } + } + + @objc public var optionalMutableInt64s: [String: NSNumber]? { + set { root.swiftModel.optionalMutableInt64s = newValue?.reduce(into: [:]) { $0[$1.0] = $1.1.int64Value } + get { root.swiftModel.optionalMutableInt64s as [String: NSNumber]? } + } + } + + """ + + let actual = try printSwiftWithObjcInterop(for: [fooStruct]) + + XCTAssertEqual(expected, actual) + } + // MARK: - Nested Swift Structs and Enums func testPrintingObjcInteropForSwiftStructWithNestedStructs() throws { @@ -1575,7 +1743,7 @@ extension SwiftStruct.Property { type: type, isOptional: isOptional, isMutable: isMutable, - defaultVaule: nil, + defaultValue: nil, codingKey: propertyName ) } diff --git a/tools/rum-models-generator/Tests/rum-models-generator-coreTests/Output/SwiftPrinterTests.swift b/tools/rum-models-generator/Tests/rum-models-generator-coreTests/Output/SwiftPrinterTests.swift index a73dbf957a..8e1315b732 100644 --- a/tools/rum-models-generator/Tests/rum-models-generator-coreTests/Output/SwiftPrinterTests.swift +++ b/tools/rum-models-generator/Tests/rum-models-generator-coreTests/Output/SwiftPrinterTests.swift @@ -25,7 +25,7 @@ final class SwiftPrinterTests: XCTestCase { type: SwiftPrimitive(), isOptional: true, isMutable: false, - defaultVaule: nil, + defaultValue: nil, codingKey: "property1" ), SwiftStruct.Property( @@ -34,7 +34,7 @@ final class SwiftPrinterTests: XCTestCase { type: SwiftPrimitive(), isOptional: false, isMutable: true, - defaultVaule: nil, + defaultValue: nil, codingKey: "property2" ) ], @@ -42,7 +42,7 @@ final class SwiftPrinterTests: XCTestCase { ), isOptional: true, isMutable: false, - defaultVaule: nil, + defaultValue: nil, codingKey: "bar" ), SwiftStruct.Property( @@ -61,7 +61,7 @@ final class SwiftPrinterTests: XCTestCase { ), isOptional: false, isMutable: false, - defaultVaule: SwiftEnum.Case(label: "case2", rawValue: "case2"), + defaultValue: SwiftEnum.Case(label: "case2", rawValue: "case2"), codingKey: "bizz" ), SwiftStruct.Property( @@ -82,8 +82,17 @@ final class SwiftPrinterTests: XCTestCase { ), isOptional: true, isMutable: true, - defaultVaule: nil, + defaultValue: nil, codingKey: "buzz" + ), + SwiftStruct.Property( + name: "propertiesByNames", + comment: "Description of FooBar's `propertiesByNames`.", + type: SwiftDictionary(value: SwiftPrimitive()), + isOptional: true, + isMutable: false, + defaultValue: nil, + codingKey: "propertiesByNames" ) ], conformance: [SwiftProtocol(name: "RUMDataModel", conformance: [codableProtocol])] @@ -117,10 +126,14 @@ final class SwiftPrinterTests: XCTestCase { /// Description of FooBar's `buzz`. public var buzz: [Buzz]? + /// Description of FooBar's `propertiesByNames`. + public let propertiesByNames: [String: String]? + enum CodingKeys: String, CodingKey { case bar = "bar" case bizz = "bizz" case buzz = "buzz" + case propertiesByNames = "propertiesByNames" } /// Description of Bar. diff --git a/tools/rum-models-generator/Tests/rum-models-generator-coreTests/RUM/RUMSwiftTypeTransformerTests.swift b/tools/rum-models-generator/Tests/rum-models-generator-coreTests/RUM/RUMSwiftTypeTransformerTests.swift index f774889fcf..4bac2408da 100644 --- a/tools/rum-models-generator/Tests/rum-models-generator-coreTests/RUM/RUMSwiftTypeTransformerTests.swift +++ b/tools/rum-models-generator/Tests/rum-models-generator-coreTests/RUM/RUMSwiftTypeTransformerTests.swift @@ -26,7 +26,7 @@ final class RUMSwiftTypeTransformerTests: XCTestCase { type: SwiftPrimitive(), isOptional: true, isMutable: false, - defaultVaule: nil, + defaultValue: nil, codingKey: "property1" ), SwiftStruct.Property( @@ -35,7 +35,7 @@ final class RUMSwiftTypeTransformerTests: XCTestCase { type: SwiftPrimitive(), isOptional: false, isMutable: true, - defaultVaule: nil, + defaultValue: nil, codingKey: "property2" ) ], @@ -43,7 +43,7 @@ final class RUMSwiftTypeTransformerTests: XCTestCase { ), isOptional: true, isMutable: false, - defaultVaule: nil, + defaultValue: nil, codingKey: "bar" ), SwiftStruct.Property( @@ -62,7 +62,7 @@ final class RUMSwiftTypeTransformerTests: XCTestCase { ), isOptional: false, isMutable: false, - defaultVaule: SwiftEnum.Case(label: "case2", rawValue: "case2"), + defaultValue: SwiftEnum.Case(label: "case2", rawValue: "case2"), codingKey: "property1" ), SwiftStruct.Property( @@ -83,8 +83,17 @@ final class RUMSwiftTypeTransformerTests: XCTestCase { ), isOptional: true, isMutable: true, - defaultVaule: nil, + defaultValue: nil, codingKey: "property2" + ), + SwiftStruct.Property( + name: "propertiesByNames", + comment: "Description of Foobar's `propertiesByNames`", + type: SwiftDictionary(value: SwiftPrimitive()), + isOptional: true, + isMutable: true, + defaultValue: nil, + codingKey: "propertiesByNames" ) ], conformance: [] @@ -110,7 +119,7 @@ final class RUMSwiftTypeTransformerTests: XCTestCase { type: SwiftPrimitive(), isOptional: true, isMutable: false, - defaultVaule: nil, + defaultValue: nil, codingKey: "property1" ), SwiftStruct.Property( @@ -119,7 +128,7 @@ final class RUMSwiftTypeTransformerTests: XCTestCase { type: SwiftPrimitive(), isOptional: false, isMutable: true, - defaultVaule: nil, + defaultValue: nil, codingKey: "property2" ) ], @@ -127,7 +136,7 @@ final class RUMSwiftTypeTransformerTests: XCTestCase { ), isOptional: true, isMutable: false, - defaultVaule: nil, + defaultValue: nil, codingKey: "bar" ), SwiftStruct.Property( @@ -146,7 +155,7 @@ final class RUMSwiftTypeTransformerTests: XCTestCase { ), isOptional: false, isMutable: false, - defaultVaule: SwiftEnum.Case(label: "case2", rawValue: "case2"), + defaultValue: SwiftEnum.Case(label: "case2", rawValue: "case2"), codingKey: "property1" ), SwiftStruct.Property( @@ -167,8 +176,17 @@ final class RUMSwiftTypeTransformerTests: XCTestCase { ), isOptional: true, isMutable: true, - defaultVaule: nil, + defaultValue: nil, codingKey: "property2" + ), + SwiftStruct.Property( + name: "propertiesByNames", + comment: "Description of Foobar's `propertiesByNames`", + type: SwiftDictionary(value: SwiftPrimitive()), + isOptional: true, + isMutable: true, + defaultValue: nil, + codingKey: "propertiesByNames" ) ], conformance: [SwiftProtocol(name: "RUMDataModel", conformance: [codableProtocol])] @@ -194,7 +212,7 @@ final class RUMSwiftTypeTransformerTests: XCTestCase { ), isOptional: true, isMutable: false, - defaultVaule: nil, + defaultValue: nil, codingKey: "connectivity" ), SwiftStruct.Property( @@ -208,7 +226,7 @@ final class RUMSwiftTypeTransformerTests: XCTestCase { ), isOptional: true, isMutable: false, - defaultVaule: nil, + defaultValue: nil, codingKey: "usr" ), SwiftStruct.Property( @@ -222,7 +240,7 @@ final class RUMSwiftTypeTransformerTests: XCTestCase { ), isOptional: true, isMutable: false, - defaultVaule: nil, + defaultValue: nil, codingKey: "method" ) ], @@ -242,7 +260,7 @@ final class RUMSwiftTypeTransformerTests: XCTestCase { type: SwiftTypeReference(referencedTypeName: "RUMConnectivity"), isOptional: true, isMutable: false, - defaultVaule: nil, + defaultValue: nil, codingKey: "connectivity" ), SwiftStruct.Property( @@ -251,7 +269,7 @@ final class RUMSwiftTypeTransformerTests: XCTestCase { type: SwiftTypeReference(referencedTypeName: "RUMUser"), isOptional: true, isMutable: false, - defaultVaule: nil, + defaultValue: nil, codingKey: "usr" ), SwiftStruct.Property( @@ -260,7 +278,7 @@ final class RUMSwiftTypeTransformerTests: XCTestCase { type: SwiftTypeReference(referencedTypeName: "RUMMethod"), isOptional: true, isMutable: false, - defaultVaule: nil, + defaultValue: nil, codingKey: "method" ) ],