diff --git a/README.md b/README.md index 083cf12..190b62d 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,14 @@ If you are new to Cocoapods, please refer to [CocoaPods documentation](https://g "page_chapter3": "chapter 3" ])) ``` + or + ```swift + pa.sendEvent(Event(PA.EventName.Page.Display, properties: Set([ + try! Property("page", "name"), + try! Property("enabled", true), + try! Property("count", "1", forceType: .int) + ]))) + ``` _For more examples, please refer to the [Documentation](https://developers.atinternet-solutions.com/piano-analytics/)_ diff --git a/Sources/PianoAnalytics/Core/Error.swift b/Sources/PianoAnalytics/Core/Error.swift index d470fa9..a182e51 100644 --- a/Sources/PianoAnalytics/Core/Error.swift +++ b/Sources/PianoAnalytics/Core/Error.swift @@ -1,3 +1,28 @@ +// +// Error.swift +// +// This SDK is licensed under the MIT license (MIT) +// Copyright (c) 2015- Applied Technologies Internet SAS (registration number B 403 261 258 - Trade and Companies Register of Bordeaux – France) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + import Foundation public extension PA { diff --git a/Sources/PianoAnalytics/Core/ExtendedConfiguration.swift b/Sources/PianoAnalytics/Core/ExtendedConfiguration.swift index d51f377..daf3117 100644 --- a/Sources/PianoAnalytics/Core/ExtendedConfiguration.swift +++ b/Sources/PianoAnalytics/Core/ExtendedConfiguration.swift @@ -1,3 +1,28 @@ +// +// ExtendedConfiguration.swift +// +// This SDK is licensed under the MIT license (MIT) +// Copyright (c) 2015- Applied Technologies Internet SAS (registration number B 403 261 258 - Trade and Companies Register of Bordeaux – France) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + import Foundation extension PA { diff --git a/Sources/PianoAnalytics/Event.swift b/Sources/PianoAnalytics/Event.swift index 1944284..034bb94 100644 --- a/Sources/PianoAnalytics/Event.swift +++ b/Sources/PianoAnalytics/Event.swift @@ -30,10 +30,19 @@ public final class Event { public final let name: String public final let data: [String: Any] - public init(_ name: String, data: [String: Any] = [:]) { + public init(_ name: String, data: [String: Any]) { self.name = name self.data = PianoAnalyticsUtils.toFlatten(src: data) } + + public init(_ name: String) { + self.name = name + self.data = [:] + } + + public convenience init(_ name: String, properties: Set) { + self.init(name, data: properties.toMap()) + } final func toMap(context: [String: ContextProperty] = [:]) -> [String: Any] { diff --git a/Sources/PianoAnalytics/Property.swift b/Sources/PianoAnalytics/Property.swift new file mode 100644 index 0000000..f1ce56b --- /dev/null +++ b/Sources/PianoAnalytics/Property.swift @@ -0,0 +1,114 @@ +// +// Property.swift +// +// This SDK is licensed under the MIT license (MIT) +// Copyright (c) 2015- Applied Technologies Internet SAS (registration number B 403 261 258 - Trade and Companies Register of Bordeaux – France) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +public final class Property { + + private static let nameExpression = try! NSRegularExpression(pattern: "^[a-z]\\w*$") + + private static func check(name: String) -> Bool { + Property.nameExpression.matches(in: name, range: NSRange(name.startIndex..., in: name)).count > 0 + } + + fileprivate let name: String + fileprivate let lowerCasedName: String + + internal let value: Any + + private init(name: String, value: Any, forceType: PA.PropertyType? = nil) throws { + self.lowerCasedName = name.lowercased() + guard name.count <= 40 + && !lowerCasedName.hasPrefix("m_") + && !lowerCasedName.hasPrefix("visit_") + && (name == "*" || Property.check(name: lowerCasedName)) else { + throw PA.Err( + message: + "Property name can contain only `a-z`, `0-9`, `_`, " + + "must begin with `a-z`, " + + "must not begin with m_ or visit_. " + + "Max allowed length: 40" + ) + } + self.name = forceType?.rawValue.appending(":\(name)") ?? name + self.value = value + } + + public convenience init(_ name: String, _ value: Bool, forceType: PA.PropertyType? = nil) throws { + try self.init(name: name, value: value, forceType: forceType) + } + + convenience init(_ name: String, _ value: Int, forceType: PA.PropertyType? = nil) throws { + try self.init(name: name, value: value, forceType: forceType) + } + + convenience init(_ name: String, _ value: Int64, forceType: PA.PropertyType? = nil) throws { + try self.init(name: name, value: value, forceType: forceType) + } + + convenience init(_ name: String, _ value: Double, forceType: PA.PropertyType? = nil) throws { + try self.init(name: name, value: value, forceType: forceType) + } + + convenience init(_ name: String, _ value: String, forceType: PA.PropertyType? = nil) throws { + try self.init(name: name, value: value, forceType: forceType) + } + + convenience init(_ name: String, _ value: Date) throws { + try self.init(name: name, value: Int64(value.timeIntervalSince1970), forceType: .date) + } + + convenience init(_ name: String, _ value: [Int], forceType: PA.PropertyType? = nil) throws { + try self.init(name: name, value: value, forceType: forceType) + } + + convenience init(_ name: String, _ value: [Double], forceType: PA.PropertyType? = nil) throws { + try self.init(name: name, value: value, forceType: forceType) + } + + convenience init(_ name: String, _ value: [String], forceType: PA.PropertyType? = nil) throws { + try self.init(name: name, value: value, forceType: forceType) + } +} + +extension Property: Hashable { + + public static func == (lhs: Property, rhs: Property) -> Bool { + lhs.lowerCasedName == rhs.lowerCasedName + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(lowerCasedName) + } +} + +internal extension Set where Element == Property { + + func toMap() -> [String:Any] { + Dictionary( + uniqueKeysWithValues: self.map { ($0.name, $0.value) } + ) + } +} diff --git a/Tests/PianoAnalyticsTests/PropertyTests.swift b/Tests/PianoAnalyticsTests/PropertyTests.swift new file mode 100644 index 0000000..0d49e5a --- /dev/null +++ b/Tests/PianoAnalyticsTests/PropertyTests.swift @@ -0,0 +1,103 @@ +// +// PropertyTests.swift +// PianoAnalyticsTests +// +// This SDK is licensed under the MIT license (MIT) +// Copyright (c) 2015- Applied Technologies Internet SAS (registration number B 403 261 258 - Trade and Companies Register of Bordeaux – France) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import XCTest +@testable import PianoAnalytics + +class PropertyTests: XCTestCase { + + func testName() throws { + XCTAssertNoThrow(try Property("valid_name", true)) + XCTAssertNoThrow(try Property("Valid_Name", true)) + XCTAssertNoThrow(try Property("ValidName", true)) + XCTAssertThrowsError(try Property("invalid-name", true)) + } + + func testValues() throws { + let now = Date() + + let properties = Set([ + try Property("bool", true), + try Property("bool_force", "true", forceType: .bool), + try Property("int", 1), + try Property("int_force", "1", forceType: .int), + try Property("int64", Int64.max), + try Property("int64_force", "\(Int64.max)", forceType: .int), + try Property("double", 1.1), + try Property("double_force", "1.1", forceType: .float), + try Property("string", "value"), + try Property("string_force", 1, forceType: .string), + try Property("date", now), + try Property("int_array", [1]), + try Property("int_array_force", "1", forceType: .intArray), + try Property("double_array", [1.1]), + try Property("double_array_force", "1", forceType: .floatArray), + try Property("string_array", ["value"]), + try Property("string_array_force", 1, forceType: .stringArray), + ]).toMap() + + XCTAssertEqual(properties["bool"] as? Bool, true) + XCTAssertEqual(properties["b:bool_force"] as? String, "true") + XCTAssertEqual(properties["int"] as? Int, 1) + XCTAssertEqual(properties["n:int_force"] as? String, "1") + XCTAssertEqual(properties["int64"] as? Int64, Int64.max) + XCTAssertEqual(properties["n:int64_force"] as? String, "\(Int64.max)") + XCTAssertEqual(properties["double"] as? Double, 1.1) + XCTAssertEqual(properties["f:double_force"] as? String, "1.1") + XCTAssertEqual(properties["string"] as? String, "value") + XCTAssertEqual(properties["s:string_force"] as? Int, 1) + XCTAssertEqual(properties["d:date"] as? Int64, Int64(now.timeIntervalSince1970)) + XCTAssertEqual(properties["int_array"] as? [Int], [1]) + XCTAssertEqual(properties["a:n:int_array_force"] as? String, "1") + XCTAssertEqual(properties["double_array"] as? [Double], [1.1]) + XCTAssertEqual(properties["a:f:double_array_force"] as? String, "1") + XCTAssertEqual(properties["string_array"] as? [String], ["value"]) + XCTAssertEqual(properties["a:s:string_array_force"] as? Int, 1) + } + + func testDuplicate() throws { + var set = Set([ + try Property("value", 1), + try Property("value", 2) + ]) + + set.insert(try Property("value", 3)) + + XCTAssertEqual(set.count, 1) + XCTAssertEqual(set.first?.value as? Int, 1) + } +} + +func nnn() { + +pa.sendEvent( + Event(PA.EventName.Page.Display, properties: Set([ + try! Property("page", "name"), + try! Property("enabled", true), + try! Property("count", "1", forceType: .int) + ])) +) +}