From 47ca98503a980cd7e1aa2d4e865cb9692a7f43d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Wed, 9 Aug 2023 14:24:37 +0200 Subject: [PATCH 1/7] Add option to reply with a Encodable type. --- Source/BridgeComponent.swift | 20 ++++++++++++++++++++ Source/JsonDataDecoder.swift | 1 + Source/Message.swift | 18 ++++++++++++++++++ Tests/BridgeComponentTests.swift | 12 ++++++++++++ 4 files changed, 51 insertions(+) diff --git a/Source/BridgeComponent.swift b/Source/BridgeComponent.swift index 6afc484..ce53baa 100644 --- a/Source/BridgeComponent.swift +++ b/Source/BridgeComponent.swift @@ -83,6 +83,26 @@ open class BridgeComponent: BridgingComponent { return reply(with: messageReply) } + @discardableResult + /// Replies to the web with the last received message for a given `event`, replacing its `jsonData`. + /// + /// NOTE: If a message has not been received for the given `event`, the reply will be ignored. + /// + /// - Parameters: + /// - event: The `event` for which a reply should be sent. + /// - jsonData: The `jsonData` to be included in the reply message. + /// - Returns: `true` if the reply was successful, `false` if the event message was not received. + public func reply(to event: String, encodable: T) -> Bool { + guard let message = receivedMessage(for: event) else { + debugLog("bridgeMessageFailedToReply: message for event \(event) was not received") + return false + } + + let messageReply = message.replacing(jsonEncodableData: encodable) + + return reply(with: messageReply) + } + /// Returns the last received message for a given `event`, if available. /// - Parameter event: The event name. /// - Returns: The last received message, or nil. diff --git a/Source/JsonDataDecoder.swift b/Source/JsonDataDecoder.swift index 4f8171a..8ee5180 100644 --- a/Source/JsonDataDecoder.swift +++ b/Source/JsonDataDecoder.swift @@ -2,4 +2,5 @@ import Foundation public struct JsonDataDecoder { public static var appDecoder: JSONDecoder = JSONDecoder() + public static var appEncoder: JSONEncoder = JSONEncoder() } diff --git a/Source/Message.swift b/Source/Message.swift index 60735bf..924a668 100644 --- a/Source/Message.swift +++ b/Source/Message.swift @@ -45,6 +45,24 @@ public struct Message: Equatable { metadata: metadata, jsonData: updatedData ?? jsonData) } + + public func replacing(event updatedEvent: String? = nil, + jsonEncodableData encodable: T? = nil) -> Message { + + let data: String? + if let encodable, + let jsonData = try? JsonDataDecoder.appEncoder.encode(encodable) { + data = String(data: jsonData, encoding: .utf8) + } else { + data = nil + } + + return Message(id: id, + component: component, + event: updatedEvent ?? event, + metadata: metadata, + jsonData: data ?? jsonData) + } } extension Message { diff --git a/Tests/BridgeComponentTests.swift b/Tests/BridgeComponentTests.swift index b78f50b..7ef37a4 100644 --- a/Tests/BridgeComponentTests.swift +++ b/Tests/BridgeComponentTests.swift @@ -66,6 +66,18 @@ class BridgeComponentTest: XCTestCase { XCTAssertEqual(bridge.replyWithMessageArg, message) } + func test_replyToReceivedMessageWithACodableSucceeds() { + let messageData = MessageData(title: "hey", subtitle: "", actionName: "tap") + let newJsonData = "{\"title\":\"hey\",\"subtitle\":\"\",\"actionName\":\"tap\"}" + let newMessage = message.replacing(jsonData: newJsonData) + + let success = component.reply(to: "connect", encodable: messageData) + + XCTAssertTrue(success) + XCTAssertTrue(bridge.replyWithMessageWasCalled) + XCTAssertEqual(bridge.replyWithMessageArg, newMessage) + } + func test_replyToMessageNotReceivedIgnoresTheReply() { let success = component.reply(to: "disconnect") From b7aae19a05ecfd246c15108d72fb0178354d9534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Wed, 9 Aug 2023 16:59:16 +0200 Subject: [PATCH 2/7] Tweak Message's replacing with an encodable object function. Add tests for replyTo with a Codable. --- Source/BridgeComponent.swift | 9 ++- Source/JsonDataDecoder.swift | 6 -- Source/Message.swift | 22 ++++-- Source/StradaJSONCoding.swift | 21 ++++++ Strada.xcodeproj/project.pbxproj | 8 +- Tests/BridgeComponentTests.swift | 12 ++- Tests/MessageTests.swift | 121 ++++++++++++++++++++++++++++++- 7 files changed, 176 insertions(+), 23 deletions(-) delete mode 100644 Source/JsonDataDecoder.swift create mode 100644 Source/StradaJSONCoding.swift diff --git a/Source/BridgeComponent.swift b/Source/BridgeComponent.swift index ce53baa..18cf997 100644 --- a/Source/BridgeComponent.swift +++ b/Source/BridgeComponent.swift @@ -84,21 +84,22 @@ open class BridgeComponent: BridgingComponent { } @discardableResult - /// Replies to the web with the last received message for a given `event`, replacing its `jsonData`. + /// Replies to the web with the last received message for a given `event`, replacing its `jsonData` + /// with the provided `Encodable` object. /// /// NOTE: If a message has not been received for the given `event`, the reply will be ignored. /// /// - Parameters: /// - event: The `event` for which a reply should be sent. - /// - jsonData: The `jsonData` to be included in the reply message. + /// - encodable: An instance conforming to `Encodable` to be included as `jsonData` in the reply message. /// - Returns: `true` if the reply was successful, `false` if the event message was not received. - public func reply(to event: String, encodable: T) -> Bool { + public func reply(to event: String, encodable: T) -> Bool { guard let message = receivedMessage(for: event) else { debugLog("bridgeMessageFailedToReply: message for event \(event) was not received") return false } - let messageReply = message.replacing(jsonEncodableData: encodable) + let messageReply = message.replacing(encodedDataObject: encodable) return reply(with: messageReply) } diff --git a/Source/JsonDataDecoder.swift b/Source/JsonDataDecoder.swift deleted file mode 100644 index 8ee5180..0000000 --- a/Source/JsonDataDecoder.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Foundation - -public struct JsonDataDecoder { - public static var appDecoder: JSONDecoder = JSONDecoder() - public static var appEncoder: JSONEncoder = JSONEncoder() -} diff --git a/Source/Message.swift b/Source/Message.swift index 924a668..dd6483b 100644 --- a/Source/Message.swift +++ b/Source/Message.swift @@ -46,13 +46,23 @@ public struct Message: Equatable { jsonData: updatedData ?? jsonData) } + /// Replaces the existing `Message`'s data with passed-in `Encodable` object and event. + /// - Parameters: + /// - updatedEvent: The updated event of this message. If omitted, the existing event is used. + /// - encodable: An instance conforming to `Encodable` to be included as data in the message. + /// If omitted, the existing data is used. + /// - Returns: A new `Message` with the provided data. public func replacing(event updatedEvent: String? = nil, - jsonEncodableData encodable: T? = nil) -> Message { - + encodedDataObject encodable: T? = nil) -> Message { let data: String? - if let encodable, - let jsonData = try? JsonDataDecoder.appEncoder.encode(encodable) { - data = String(data: jsonData, encoding: .utf8) + if let encodable { + do { + let jsonData = try StradaJSONEncoder.appEncoder.encode(encodable) + data = String(data: jsonData, encoding: .utf8) + } catch { + debugLog("Error encoding codable object: \(encodable) -> \(error)") + data = nil + } } else { data = nil } @@ -79,7 +89,7 @@ extension Message { } do { - let decoder = JsonDataDecoder.appDecoder + let decoder = StradaJSONDecoder.appDecoder return try decoder.decode(T.self, from: data) } catch { debugLog("Error decoding json: \(jsonData) -> \(error)") diff --git a/Source/StradaJSONCoding.swift b/Source/StradaJSONCoding.swift new file mode 100644 index 0000000..3448d1e --- /dev/null +++ b/Source/StradaJSONCoding.swift @@ -0,0 +1,21 @@ +import Foundation + +/// A utility for managing custom JSON encoders for the library. +/// +/// The `StradaJSONEncoder` enum provides a static property `appEncoder` +/// that allows users to set a custom JSON encoder for the library. +/// The custom encoder can be useful when you need to apply specific +/// encoding strategies. +public enum StradaJSONEncoder { + public static var appEncoder: JSONEncoder = JSONEncoder() +} + +/// A utility for managing custom JSON decoders for the library. +/// +/// The `StradaJSONDecoder` enum provides a static property `appDecoder` +/// that allows users to set a custom JSON decoder for the library. +/// The custom decoder can be useful when you need to apply specific +/// decoding strategies. +public enum StradaJSONDecoder { + public static var appDecoder: JSONDecoder = JSONDecoder() +} diff --git a/Strada.xcodeproj/project.pbxproj b/Strada.xcodeproj/project.pbxproj index cbd30dc..5bfe967 100644 --- a/Strada.xcodeproj/project.pbxproj +++ b/Strada.xcodeproj/project.pbxproj @@ -18,7 +18,7 @@ C1EB05262588133D00933244 /* MessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EB05252588133D00933244 /* MessageTests.swift */; }; C1EB052E2588201600933244 /* BridgeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EB052D2588201600933244 /* BridgeTests.swift */; }; CBAFC52926F9863900C6662E /* PathLoaderXcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAFC52826F9863900C6662E /* PathLoaderXcode.swift */; }; - E200E7D12A814D4500E41FA9 /* JsonDataDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = E200E7D02A814D4500E41FA9 /* JsonDataDecoder.swift */; }; + E200E7D12A814D4500E41FA9 /* StradaJSONCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = E200E7D02A814D4500E41FA9 /* StradaJSONCoding.swift */; }; E20978422A6E9E6B00CDEEE5 /* InternalMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20978412A6E9E6B00CDEEE5 /* InternalMessage.swift */; }; E20978442A6EAF3600CDEEE5 /* InternalMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20978432A6EAF3600CDEEE5 /* InternalMessageTests.swift */; }; E20978472A7135E700CDEEE5 /* Encodable+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20978462A7135E700CDEEE5 /* Encodable+Utils.swift */; }; @@ -59,7 +59,7 @@ C1EB05252588133D00933244 /* MessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageTests.swift; sourceTree = ""; }; C1EB052D2588201600933244 /* BridgeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BridgeTests.swift; sourceTree = ""; }; CBAFC52826F9863900C6662E /* PathLoaderXcode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PathLoaderXcode.swift; sourceTree = ""; }; - E200E7D02A814D4500E41FA9 /* JsonDataDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonDataDecoder.swift; sourceTree = ""; }; + E200E7D02A814D4500E41FA9 /* StradaJSONCoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StradaJSONCoding.swift; sourceTree = ""; }; E20978412A6E9E6B00CDEEE5 /* InternalMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalMessage.swift; sourceTree = ""; }; E20978432A6EAF3600CDEEE5 /* InternalMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalMessageTests.swift; sourceTree = ""; }; E20978462A7135E700CDEEE5 /* Encodable+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Encodable+Utils.swift"; sourceTree = ""; }; @@ -127,7 +127,7 @@ E20978412A6E9E6B00CDEEE5 /* InternalMessage.swift */, E2DB15902A7163B0001EE08C /* BridgeDelegate.swift */, E2DB15922A7282CF001EE08C /* BridgeComponent.swift */, - E200E7D02A814D4500E41FA9 /* JsonDataDecoder.swift */, + E200E7D02A814D4500E41FA9 /* StradaJSONCoding.swift */, ); path = Source; sourceTree = ""; @@ -282,7 +282,7 @@ files = ( E209784B2A714D4E00CDEEE5 /* String+JSON.swift in Sources */, C11349A62587EFFB000A6E56 /* ScriptMessageHandler.swift in Sources */, - E200E7D12A814D4500E41FA9 /* JsonDataDecoder.swift in Sources */, + E200E7D12A814D4500E41FA9 /* StradaJSONCoding.swift in Sources */, E20978472A7135E700CDEEE5 /* Encodable+Utils.swift in Sources */, E2DB15912A7163B0001EE08C /* BridgeDelegate.swift in Sources */, C11349B22587F31E000A6E56 /* JavaScript.swift in Sources */, diff --git a/Tests/BridgeComponentTests.swift b/Tests/BridgeComponentTests.swift index 7ef37a4..70ddf01 100644 --- a/Tests/BridgeComponentTests.swift +++ b/Tests/BridgeComponentTests.swift @@ -66,7 +66,7 @@ class BridgeComponentTest: XCTestCase { XCTAssertEqual(bridge.replyWithMessageArg, message) } - func test_replyToReceivedMessageWithACodableSucceeds() { + func test_replyToReceivedMessageWithACodableObjectSucceeds() { let messageData = MessageData(title: "hey", subtitle: "", actionName: "tap") let newJsonData = "{\"title\":\"hey\",\"subtitle\":\"\",\"actionName\":\"tap\"}" let newMessage = message.replacing(jsonData: newJsonData) @@ -78,6 +78,16 @@ class BridgeComponentTest: XCTestCase { XCTAssertEqual(bridge.replyWithMessageArg, newMessage) } + func test_replyToMessageNotReceivedWithACodableObjectIgnoresTheReply() { + let messageData = MessageData(title: "hey", subtitle: "", actionName: "tap") + + let success = component.reply(to: "disconnect", encodable: messageData) + + XCTAssertFalse(success) + XCTAssertFalse(bridge.replyWithMessageWasCalled) + XCTAssertNil(bridge.replyWithMessageArg) + } + func test_replyToMessageNotReceivedIgnoresTheReply() { let success = component.reply(to: "disconnect") diff --git a/Tests/MessageTests.swift b/Tests/MessageTests.swift index 36f9149..3637b99 100644 --- a/Tests/MessageTests.swift +++ b/Tests/MessageTests.swift @@ -2,6 +2,16 @@ import XCTest @testable import Strada class MessageTests: XCTestCase { + + private let metadata = Message.Metadata(url: "https://37signals.com") + + override func setUp() async throws { + StradaJSONEncoder.appEncoder = JSONEncoder() + StradaJSONDecoder.appDecoder = JSONDecoder() + } + + // MARK: replacing(event:, jsonData:) + func testReplacingWithNewEventAndData() { let metadata = Message.Metadata(url: "https://37signals.com") let jsonData = """ @@ -86,6 +96,89 @@ class MessageTests: XCTestCase { XCTAssertEqual(newMessage.jsonData, jsonData) } + // MARK: replacing(event:, encodedDataObject:) + + func testReplacingWithNewEventAndEncodable() { + let metadata = Message.Metadata(url: "https://37signals.com") + let newEvent = "disconnect" + let message = Message(id: "1", + component: "page", + event: "connect", + metadata: metadata, + jsonData: "{}") + let messageData = MessageData(title: "hey", subtitle: "", actionName: "tap") + let newJsonData = "{\"title\":\"hey\",\"subtitle\":\"\",\"actionName\":\"tap\"}" + + let newMessage = message.replacing(event: newEvent, encodedDataObject: messageData) + + XCTAssertEqual(newMessage.id, "1") + XCTAssertEqual(newMessage.component, "page") + XCTAssertEqual(newMessage.event, newEvent) + XCTAssertEqual(newMessage.metadata, metadata) + XCTAssertEqual(newMessage.jsonData, newJsonData) + } + + func testReplacingByChangingEncodableWithoutChangingEvent() { + let metadata = Message.Metadata(url: "https://37signals.com") + let message = Message(id: "1", + component: "page", + event: "connect", + metadata: metadata, + jsonData: "{\"title\":\"Page-title\"}") + let messageData = MessageData(title: "hey", subtitle: "", actionName: "tap") + let newJsonData = "{\"title\":\"hey\",\"subtitle\":\"\",\"actionName\":\"tap\"}" + + let newMessage = message.replacing(encodedDataObject: messageData) + + XCTAssertEqual(newMessage.id, "1") + XCTAssertEqual(newMessage.component, "page") + XCTAssertEqual(newMessage.event, "connect") + XCTAssertEqual(newMessage.metadata, metadata) + XCTAssertEqual(newMessage.jsonData, newJsonData) + } + + func testReplacingByChangingEventWithoutChangingEncodable() { + let jsonData = """ + {"title":"Page-title","subtitle":"Page-subtitle"} + """ + let message = Message(id: "1", + component: "page", + event: "connect", + metadata: metadata, + jsonData: jsonData) + let newEvent = "disconnect" + let messageData: MessageData? = nil + let newMessage = message.replacing(event: newEvent, encodedDataObject: messageData) + + XCTAssertEqual(newMessage.id, "1") + XCTAssertEqual(newMessage.component, "page") + XCTAssertEqual(newMessage.event, newEvent) + XCTAssertEqual(newMessage.metadata, metadata) + XCTAssertEqual(newMessage.jsonData, jsonData) + } + + func testReplacingWithoutChangingEventAndEncodable() { + let jsonData = """ + {"title":"Page-title","subtitle":"Page-subtitle"} + """ + let message = Message(id: "1", + component: "page", + event: "connect", + metadata: metadata, + jsonData: jsonData) + + let messageData: MessageData? = nil + let newMessage = message.replacing(event: nil, encodedDataObject: messageData) + + XCTAssertEqual(newMessage.id, "1") + XCTAssertEqual(newMessage.component, "page") + XCTAssertEqual(newMessage.event, "connect") + XCTAssertEqual(newMessage.metadata, metadata) + XCTAssertEqual(newMessage.jsonData, jsonData) + } + + // MARK: Decoding + func test_decodingWithDefaultDecoder() { let metadata = Message.Metadata(url: "https://37signals.com") let jsonData = """ @@ -109,9 +202,8 @@ class MessageTests: XCTestCase { func test_decodingWithCustomDecoder() { let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase - JsonDataDecoder.appDecoder = decoder + StradaJSONDecoder.appDecoder = decoder - let metadata = Message.Metadata(url: "https://37signals.com") let jsonData = """ {"title":"Page-title","subtitle":"Page-subtitle", "action_name": "go"} """ @@ -129,4 +221,29 @@ class MessageTests: XCTestCase { XCTAssertEqual(decodedMessageData, pageData) } + + // MARK: Custom encoding + + func test_encodingWithCustomEncoder() { + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + StradaJSONEncoder.appEncoder = encoder + + let messageData = MessageData(title: "Page-title", + subtitle: "Page-subtitle", + actionName: "go") + + let jsonData = """ + {"title":"Page-title","subtitle":"Page-subtitle","action_name":"go"} + """ + let message = Message(id: "1", + component: "page", + event: "connect", + metadata: metadata, + jsonData: jsonData) + + let newMessage = message.replacing(encodedDataObject: messageData) + + XCTAssertEqual(message, newMessage) + } } From 74cb4b0e0ec0478076166edbc159e95d550a4eb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Wed, 9 Aug 2023 17:00:57 +0200 Subject: [PATCH 3/7] Rename Message's decodedJsonData() to decodedDataObject(). --- Source/Message.swift | 2 +- Tests/MessageTests.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/Message.swift b/Source/Message.swift index dd6483b..04482dd 100644 --- a/Source/Message.swift +++ b/Source/Message.swift @@ -82,7 +82,7 @@ extension Message { } extension Message { - public func decodedJsonData() -> T? { + public func decodedDataObject() -> T? { guard let data = jsonData.data(using: .utf8) else { debugLog("Error converting json string to data: \(jsonData)") return nil diff --git a/Tests/MessageTests.swift b/Tests/MessageTests.swift index 3637b99..24ec9f1 100644 --- a/Tests/MessageTests.swift +++ b/Tests/MessageTests.swift @@ -194,7 +194,7 @@ class MessageTests: XCTestCase { subtitle: "Page-subtitle", actionName: "go") - let decodedMessageData: MessageData? = message.decodedJsonData() + let decodedMessageData: MessageData? = message.decodedDataObject() XCTAssertEqual(decodedMessageData, pageData) } @@ -217,7 +217,7 @@ class MessageTests: XCTestCase { subtitle: "Page-subtitle", actionName: "go") - let decodedMessageData: MessageData? = message.decodedJsonData() + let decodedMessageData: MessageData? = message.decodedDataObject() XCTAssertEqual(decodedMessageData, pageData) } From 007d0d64eaac395997a55c168f9ccb5855af19c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Wed, 9 Aug 2023 17:06:51 +0200 Subject: [PATCH 4/7] Rename reply(to:) functions. --- Source/BridgeComponent.swift | 4 ++-- Tests/BridgeComponentTests.swift | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Source/BridgeComponent.swift b/Source/BridgeComponent.swift index 18cf997..cba4a8d 100644 --- a/Source/BridgeComponent.swift +++ b/Source/BridgeComponent.swift @@ -72,7 +72,7 @@ open class BridgeComponent: BridgingComponent { /// - event: The `event` for which a reply should be sent. /// - jsonData: The `jsonData` to be included in the reply message. /// - Returns: `true` if the reply was successful, `false` if the event message was not received. - public func reply(to event: String, jsonData: String) -> Bool { + public func reply(to event: String, with jsonData: String) -> Bool { guard let message = receivedMessage(for: event) else { debugLog("bridgeMessageFailedToReply: message for event \(event) was not received") return false @@ -93,7 +93,7 @@ open class BridgeComponent: BridgingComponent { /// - event: The `event` for which a reply should be sent. /// - encodable: An instance conforming to `Encodable` to be included as `jsonData` in the reply message. /// - Returns: `true` if the reply was successful, `false` if the event message was not received. - public func reply(to event: String, encodable: T) -> Bool { + public func reply(to event: String, with encodable: T) -> Bool { guard let message = receivedMessage(for: event) else { debugLog("bridgeMessageFailedToReply: message for event \(event) was not received") return false diff --git a/Tests/BridgeComponentTests.swift b/Tests/BridgeComponentTests.swift index 70ddf01..5b4c428 100644 --- a/Tests/BridgeComponentTests.swift +++ b/Tests/BridgeComponentTests.swift @@ -71,7 +71,7 @@ class BridgeComponentTest: XCTestCase { let newJsonData = "{\"title\":\"hey\",\"subtitle\":\"\",\"actionName\":\"tap\"}" let newMessage = message.replacing(jsonData: newJsonData) - let success = component.reply(to: "connect", encodable: messageData) + let success = component.reply(to: "connect", with: messageData) XCTAssertTrue(success) XCTAssertTrue(bridge.replyWithMessageWasCalled) @@ -81,7 +81,7 @@ class BridgeComponentTest: XCTestCase { func test_replyToMessageNotReceivedWithACodableObjectIgnoresTheReply() { let messageData = MessageData(title: "hey", subtitle: "", actionName: "tap") - let success = component.reply(to: "disconnect", encodable: messageData) + let success = component.reply(to: "disconnect", with: messageData) XCTAssertFalse(success) XCTAssertFalse(bridge.replyWithMessageWasCalled) @@ -97,7 +97,7 @@ class BridgeComponentTest: XCTestCase { } func test_replyToMessageNotReceivedWithJsonDataIgnoresTheReply() { - let success = component.reply(to: "disconnect", jsonData: "{\"title\":\"Page-title\"}") + let success = component.reply(to: "disconnect", with: "{\"title\":\"Page-title\"}") XCTAssertFalse(success) XCTAssertFalse(bridge.replyWithMessageWasCalled) From 8c7e2b65b393723fe2951470a9c2dd1cc88ed386 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Thu, 10 Aug 2023 10:20:43 +0200 Subject: [PATCH 5/7] Rename Message's functions related to Encodable types. --- Source/BridgeComponent.swift | 8 +++--- Source/Message.swift | 33 +++++++++-------------- Tests/MessageTests.swift | 52 +++++------------------------------- 3 files changed, 23 insertions(+), 70 deletions(-) diff --git a/Source/BridgeComponent.swift b/Source/BridgeComponent.swift index cba4a8d..a3254fb 100644 --- a/Source/BridgeComponent.swift +++ b/Source/BridgeComponent.swift @@ -85,21 +85,21 @@ open class BridgeComponent: BridgingComponent { @discardableResult /// Replies to the web with the last received message for a given `event`, replacing its `jsonData` - /// with the provided `Encodable` object. + /// with the provided `Encodable` object. /// /// NOTE: If a message has not been received for the given `event`, the reply will be ignored. /// /// - Parameters: /// - event: The `event` for which a reply should be sent. - /// - encodable: An instance conforming to `Encodable` to be included as `jsonData` in the reply message. + /// - data: An instance conforming to `Encodable` to be included as `jsonData` in the reply message. /// - Returns: `true` if the reply was successful, `false` if the event message was not received. - public func reply(to event: String, with encodable: T) -> Bool { + public func reply(to event: String, with data: T) -> Bool { guard let message = receivedMessage(for: event) else { debugLog("bridgeMessageFailedToReply: message for event \(event) was not received") return false } - let messageReply = message.replacing(encodedDataObject: encodable) + let messageReply = message.replacing(data: data) return reply(with: messageReply) } diff --git a/Source/Message.swift b/Source/Message.swift index 04482dd..03bef85 100644 --- a/Source/Message.swift +++ b/Source/Message.swift @@ -49,29 +49,20 @@ public struct Message: Equatable { /// Replaces the existing `Message`'s data with passed-in `Encodable` object and event. /// - Parameters: /// - updatedEvent: The updated event of this message. If omitted, the existing event is used. - /// - encodable: An instance conforming to `Encodable` to be included as data in the message. - /// If omitted, the existing data is used. + /// - data: An instance conforming to `Encodable` to be included as data in the message. /// - Returns: A new `Message` with the provided data. public func replacing(event updatedEvent: String? = nil, - encodedDataObject encodable: T? = nil) -> Message { - let data: String? - if let encodable { - do { - let jsonData = try StradaJSONEncoder.appEncoder.encode(encodable) - data = String(data: jsonData, encoding: .utf8) - } catch { - debugLog("Error encoding codable object: \(encodable) -> \(error)") - data = nil - } - } else { - data = nil + data: T) -> Message { + let updatedData: String? + do { + let jsonData = try StradaJSONEncoder.appEncoder.encode(data) + updatedData = String(data: jsonData, encoding: .utf8) + } catch { + debugLog("Error encoding codable object: \(data) -> \(error)") + updatedData = nil } - return Message(id: id, - component: component, - event: updatedEvent ?? event, - metadata: metadata, - jsonData: data ?? jsonData) + return replacing(event: updatedEvent, jsonData: updatedData) } } @@ -82,7 +73,9 @@ extension Message { } extension Message { - public func decodedDataObject() -> T? { + /// Returns a value of the type you specify, decoded from the `jsonData`. + /// - Returns: A value of the specified type, if the decoder can parse the data, otherwise nil. + public func data() -> T? { guard let data = jsonData.data(using: .utf8) else { debugLog("Error converting json string to data: \(jsonData)") return nil diff --git a/Tests/MessageTests.swift b/Tests/MessageTests.swift index 24ec9f1..5d8f959 100644 --- a/Tests/MessageTests.swift +++ b/Tests/MessageTests.swift @@ -96,7 +96,7 @@ class MessageTests: XCTestCase { XCTAssertEqual(newMessage.jsonData, jsonData) } - // MARK: replacing(event:, encodedDataObject:) + // MARK: replacing(event:, data:) func testReplacingWithNewEventAndEncodable() { let metadata = Message.Metadata(url: "https://37signals.com") @@ -109,7 +109,7 @@ class MessageTests: XCTestCase { let messageData = MessageData(title: "hey", subtitle: "", actionName: "tap") let newJsonData = "{\"title\":\"hey\",\"subtitle\":\"\",\"actionName\":\"tap\"}" - let newMessage = message.replacing(event: newEvent, encodedDataObject: messageData) + let newMessage = message.replacing(event: newEvent, data: messageData) XCTAssertEqual(newMessage.id, "1") XCTAssertEqual(newMessage.component, "page") @@ -128,7 +128,7 @@ class MessageTests: XCTestCase { let messageData = MessageData(title: "hey", subtitle: "", actionName: "tap") let newJsonData = "{\"title\":\"hey\",\"subtitle\":\"\",\"actionName\":\"tap\"}" - let newMessage = message.replacing(encodedDataObject: messageData) + let newMessage = message.replacing(data: messageData) XCTAssertEqual(newMessage.id, "1") XCTAssertEqual(newMessage.component, "page") @@ -136,46 +136,6 @@ class MessageTests: XCTestCase { XCTAssertEqual(newMessage.metadata, metadata) XCTAssertEqual(newMessage.jsonData, newJsonData) } - - func testReplacingByChangingEventWithoutChangingEncodable() { - let jsonData = """ - {"title":"Page-title","subtitle":"Page-subtitle"} - """ - let message = Message(id: "1", - component: "page", - event: "connect", - metadata: metadata, - jsonData: jsonData) - let newEvent = "disconnect" - let messageData: MessageData? = nil - let newMessage = message.replacing(event: newEvent, encodedDataObject: messageData) - - XCTAssertEqual(newMessage.id, "1") - XCTAssertEqual(newMessage.component, "page") - XCTAssertEqual(newMessage.event, newEvent) - XCTAssertEqual(newMessage.metadata, metadata) - XCTAssertEqual(newMessage.jsonData, jsonData) - } - - func testReplacingWithoutChangingEventAndEncodable() { - let jsonData = """ - {"title":"Page-title","subtitle":"Page-subtitle"} - """ - let message = Message(id: "1", - component: "page", - event: "connect", - metadata: metadata, - jsonData: jsonData) - - let messageData: MessageData? = nil - let newMessage = message.replacing(event: nil, encodedDataObject: messageData) - - XCTAssertEqual(newMessage.id, "1") - XCTAssertEqual(newMessage.component, "page") - XCTAssertEqual(newMessage.event, "connect") - XCTAssertEqual(newMessage.metadata, metadata) - XCTAssertEqual(newMessage.jsonData, jsonData) - } // MARK: Decoding @@ -194,7 +154,7 @@ class MessageTests: XCTestCase { subtitle: "Page-subtitle", actionName: "go") - let decodedMessageData: MessageData? = message.decodedDataObject() + let decodedMessageData: MessageData? = message.data() XCTAssertEqual(decodedMessageData, pageData) } @@ -217,7 +177,7 @@ class MessageTests: XCTestCase { subtitle: "Page-subtitle", actionName: "go") - let decodedMessageData: MessageData? = message.decodedDataObject() + let decodedMessageData: MessageData? = message.data() XCTAssertEqual(decodedMessageData, pageData) } @@ -242,7 +202,7 @@ class MessageTests: XCTestCase { metadata: metadata, jsonData: jsonData) - let newMessage = message.replacing(encodedDataObject: messageData) + let newMessage = message.replacing(data: messageData) XCTAssertEqual(message, newMessage) } From a76d6a5d83fdfe44d6c1ad577710ce2ae2dd2d76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Thu, 10 Aug 2023 10:23:56 +0200 Subject: [PATCH 6/7] Tweak `Message` file structure. --- Source/Message.swift | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/Source/Message.swift b/Source/Message.swift index 03bef85..de76091 100644 --- a/Source/Message.swift +++ b/Source/Message.swift @@ -19,19 +19,9 @@ public struct Message: Equatable { /// Data, represented in a json object string, to send along with the message. /// For a "page" component, this might be `{"title": "Page Title"}`. public let jsonData: String - - init(id: String, - component: String, - event: String, - metadata: Metadata?, - jsonData: String) { - self.id = id - self.component = component - self.event = event - self.metadata = metadata - self.jsonData = jsonData - } - +} + +extension Message { /// Replaces the existing `Message`'s data with passed-in data and event. /// - Parameters: /// - updatedEvent: The updated event of this message. If omitted, the existing event is used. @@ -64,15 +54,7 @@ public struct Message: Equatable { return replacing(event: updatedEvent, jsonData: updatedData) } -} - -extension Message { - public struct Metadata: Equatable { - public let url: String - } -} - -extension Message { + /// Returns a value of the type you specify, decoded from the `jsonData`. /// - Returns: A value of the specified type, if the decoder can parse the data, otherwise nil. public func data() -> T? { @@ -90,3 +72,9 @@ extension Message { } } } + +extension Message { + public struct Metadata: Equatable { + public let url: String + } +} From 04f51d09c97b2983f8ba2333afbb325e3ee0883b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Thu, 10 Aug 2023 11:01:01 +0200 Subject: [PATCH 7/7] Move json encoder/decoder configurations to a config inside a Strada namespace. --- Source/Message.swift | 4 ++-- Source/Strada.swift | 5 +++++ Source/StradaConfig.swift | 13 +++++++++++++ Source/StradaJSONCoding.swift | 21 --------------------- Strada.xcodeproj/project.pbxproj | 12 ++++++++---- Tests/MessageTests.swift | 8 ++++---- Tests/Spies/BridgeSpy.swift | 2 +- 7 files changed, 33 insertions(+), 32 deletions(-) create mode 100644 Source/Strada.swift create mode 100644 Source/StradaConfig.swift delete mode 100644 Source/StradaJSONCoding.swift diff --git a/Source/Message.swift b/Source/Message.swift index de76091..173d490 100644 --- a/Source/Message.swift +++ b/Source/Message.swift @@ -45,7 +45,7 @@ extension Message { data: T) -> Message { let updatedData: String? do { - let jsonData = try StradaJSONEncoder.appEncoder.encode(data) + let jsonData = try Strada.config.jsonEncoder.encode(data) updatedData = String(data: jsonData, encoding: .utf8) } catch { debugLog("Error encoding codable object: \(data) -> \(error)") @@ -64,7 +64,7 @@ extension Message { } do { - let decoder = StradaJSONDecoder.appDecoder + let decoder = Strada.config.jsonDecoder return try decoder.decode(T.self, from: data) } catch { debugLog("Error decoding json: \(jsonData) -> \(error)") diff --git a/Source/Strada.swift b/Source/Strada.swift new file mode 100644 index 0000000..7a437d4 --- /dev/null +++ b/Source/Strada.swift @@ -0,0 +1,5 @@ +import Foundation + +public enum Strada { + public static var config: StradaConfig = StradaConfig() +} diff --git a/Source/StradaConfig.swift b/Source/StradaConfig.swift new file mode 100644 index 0000000..441e4ce --- /dev/null +++ b/Source/StradaConfig.swift @@ -0,0 +1,13 @@ +import Foundation + +public struct StradaConfig { + /// Allows users to set a custom JSON encoder for the library. + /// The custom encoder can be useful when you need to apply specific + /// encoding strategies. + public var jsonEncoder: JSONEncoder = JSONEncoder() + + /// Allows users to set a custom JSON decoder for the library. + /// The custom decoder can be useful when you need to apply specific + /// decoding strategies. + public var jsonDecoder: JSONDecoder = JSONDecoder() +} diff --git a/Source/StradaJSONCoding.swift b/Source/StradaJSONCoding.swift deleted file mode 100644 index 3448d1e..0000000 --- a/Source/StradaJSONCoding.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation - -/// A utility for managing custom JSON encoders for the library. -/// -/// The `StradaJSONEncoder` enum provides a static property `appEncoder` -/// that allows users to set a custom JSON encoder for the library. -/// The custom encoder can be useful when you need to apply specific -/// encoding strategies. -public enum StradaJSONEncoder { - public static var appEncoder: JSONEncoder = JSONEncoder() -} - -/// A utility for managing custom JSON decoders for the library. -/// -/// The `StradaJSONDecoder` enum provides a static property `appDecoder` -/// that allows users to set a custom JSON decoder for the library. -/// The custom decoder can be useful when you need to apply specific -/// decoding strategies. -public enum StradaJSONDecoder { - public static var appDecoder: JSONDecoder = JSONDecoder() -} diff --git a/Strada.xcodeproj/project.pbxproj b/Strada.xcodeproj/project.pbxproj index 5bfe967..22628ed 100644 --- a/Strada.xcodeproj/project.pbxproj +++ b/Strada.xcodeproj/project.pbxproj @@ -18,13 +18,14 @@ C1EB05262588133D00933244 /* MessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EB05252588133D00933244 /* MessageTests.swift */; }; C1EB052E2588201600933244 /* BridgeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EB052D2588201600933244 /* BridgeTests.swift */; }; CBAFC52926F9863900C6662E /* PathLoaderXcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAFC52826F9863900C6662E /* PathLoaderXcode.swift */; }; - E200E7D12A814D4500E41FA9 /* StradaJSONCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = E200E7D02A814D4500E41FA9 /* StradaJSONCoding.swift */; }; E20978422A6E9E6B00CDEEE5 /* InternalMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20978412A6E9E6B00CDEEE5 /* InternalMessage.swift */; }; E20978442A6EAF3600CDEEE5 /* InternalMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20978432A6EAF3600CDEEE5 /* InternalMessageTests.swift */; }; E20978472A7135E700CDEEE5 /* Encodable+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20978462A7135E700CDEEE5 /* Encodable+Utils.swift */; }; E20978492A71366B00CDEEE5 /* Data+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20978482A71366B00CDEEE5 /* Data+Utils.swift */; }; E209784B2A714D4E00CDEEE5 /* String+JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = E209784A2A714D4E00CDEEE5 /* String+JSON.swift */; }; E209784D2A714F1900CDEEE5 /* Dictionary+JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = E209784C2A714F1900CDEEE5 /* Dictionary+JSON.swift */; }; + E22CBEFF2A84D7060024EFB8 /* StradaConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22CBEFE2A84D7060024EFB8 /* StradaConfig.swift */; }; + E22CBF012A84DC380024EFB8 /* Strada.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22CBF002A84DC380024EFB8 /* Strada.swift */; }; E2DB15912A7163B0001EE08C /* BridgeDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2DB15902A7163B0001EE08C /* BridgeDelegate.swift */; }; E2DB15932A7282CF001EE08C /* BridgeComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2DB15922A7282CF001EE08C /* BridgeComponent.swift */; }; E2DB15952A72B0A8001EE08C /* BridgeDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2DB15942A72B0A8001EE08C /* BridgeDelegateTests.swift */; }; @@ -59,13 +60,14 @@ C1EB05252588133D00933244 /* MessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageTests.swift; sourceTree = ""; }; C1EB052D2588201600933244 /* BridgeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BridgeTests.swift; sourceTree = ""; }; CBAFC52826F9863900C6662E /* PathLoaderXcode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PathLoaderXcode.swift; sourceTree = ""; }; - E200E7D02A814D4500E41FA9 /* StradaJSONCoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StradaJSONCoding.swift; sourceTree = ""; }; E20978412A6E9E6B00CDEEE5 /* InternalMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalMessage.swift; sourceTree = ""; }; E20978432A6EAF3600CDEEE5 /* InternalMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalMessageTests.swift; sourceTree = ""; }; E20978462A7135E700CDEEE5 /* Encodable+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Encodable+Utils.swift"; sourceTree = ""; }; E20978482A71366B00CDEEE5 /* Data+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Utils.swift"; sourceTree = ""; }; E209784A2A714D4E00CDEEE5 /* String+JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+JSON.swift"; sourceTree = ""; }; E209784C2A714F1900CDEEE5 /* Dictionary+JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+JSON.swift"; sourceTree = ""; }; + E22CBEFE2A84D7060024EFB8 /* StradaConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StradaConfig.swift; sourceTree = ""; }; + E22CBF002A84DC380024EFB8 /* Strada.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strada.swift; sourceTree = ""; }; E2DB15902A7163B0001EE08C /* BridgeDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BridgeDelegate.swift; sourceTree = ""; }; E2DB15922A7282CF001EE08C /* BridgeComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BridgeComponent.swift; sourceTree = ""; }; E2DB15942A72B0A8001EE08C /* BridgeDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BridgeDelegateTests.swift; sourceTree = ""; }; @@ -127,7 +129,8 @@ E20978412A6E9E6B00CDEEE5 /* InternalMessage.swift */, E2DB15902A7163B0001EE08C /* BridgeDelegate.swift */, E2DB15922A7282CF001EE08C /* BridgeComponent.swift */, - E200E7D02A814D4500E41FA9 /* StradaJSONCoding.swift */, + E22CBEFE2A84D7060024EFB8 /* StradaConfig.swift */, + E22CBF002A84DC380024EFB8 /* Strada.swift */, ); path = Source; sourceTree = ""; @@ -282,12 +285,13 @@ files = ( E209784B2A714D4E00CDEEE5 /* String+JSON.swift in Sources */, C11349A62587EFFB000A6E56 /* ScriptMessageHandler.swift in Sources */, - E200E7D12A814D4500E41FA9 /* StradaJSONCoding.swift in Sources */, + E22CBEFF2A84D7060024EFB8 /* StradaConfig.swift in Sources */, E20978472A7135E700CDEEE5 /* Encodable+Utils.swift in Sources */, E2DB15912A7163B0001EE08C /* BridgeDelegate.swift in Sources */, C11349B22587F31E000A6E56 /* JavaScript.swift in Sources */, 9274F20222299715003E85F4 /* Bridge.swift in Sources */, E2DB15932A7282CF001EE08C /* BridgeComponent.swift in Sources */, + E22CBF012A84DC380024EFB8 /* Strada.swift in Sources */, E20978492A71366B00CDEEE5 /* Data+Utils.swift in Sources */, 9274F20422299738003E85F4 /* Message.swift in Sources */, E20978422A6E9E6B00CDEEE5 /* InternalMessage.swift in Sources */, diff --git a/Tests/MessageTests.swift b/Tests/MessageTests.swift index 5d8f959..54fb786 100644 --- a/Tests/MessageTests.swift +++ b/Tests/MessageTests.swift @@ -6,8 +6,8 @@ class MessageTests: XCTestCase { private let metadata = Message.Metadata(url: "https://37signals.com") override func setUp() async throws { - StradaJSONEncoder.appEncoder = JSONEncoder() - StradaJSONDecoder.appDecoder = JSONDecoder() + Strada.config.jsonEncoder = JSONEncoder() + Strada.config.jsonDecoder = JSONDecoder() } // MARK: replacing(event:, jsonData:) @@ -162,7 +162,7 @@ class MessageTests: XCTestCase { func test_decodingWithCustomDecoder() { let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase - StradaJSONDecoder.appDecoder = decoder + Strada.config.jsonDecoder = decoder let jsonData = """ {"title":"Page-title","subtitle":"Page-subtitle", "action_name": "go"} @@ -187,7 +187,7 @@ class MessageTests: XCTestCase { func test_encodingWithCustomEncoder() { let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase - StradaJSONEncoder.appEncoder = encoder + Strada.config.jsonEncoder = encoder let messageData = MessageData(title: "Page-title", subtitle: "Page-subtitle", diff --git a/Tests/Spies/BridgeSpy.swift b/Tests/Spies/BridgeSpy.swift index 59956a0..9771e34 100644 --- a/Tests/Spies/BridgeSpy.swift +++ b/Tests/Spies/BridgeSpy.swift @@ -3,7 +3,7 @@ import WebKit @testable import Strada final class BridgeSpy: Bridgable { - var delegate: Strada.BridgeDelegate? = nil + var delegate: BridgeDelegate? = nil var webView: WKWebView? = nil var registerComponentWasCalled = false