Skip to content

Commit

Permalink
Merge pull request #12 from hotwired/reply-codable
Browse files Browse the repository at this point in the history
Reply with an Encodable type
  • Loading branch information
svara authored Aug 10, 2023
2 parents e3afe23 + 04f51d0 commit 03a77d5
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 39 deletions.
23 changes: 22 additions & 1 deletion Source/BridgeComponent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -83,6 +83,27 @@ 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`
/// 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.
/// - 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<T: Encodable>(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(data: data)

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.
Expand Down
5 changes: 0 additions & 5 deletions Source/JsonDataDecoder.swift

This file was deleted.

55 changes: 32 additions & 23 deletions Source/Message.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -45,27 +35,46 @@ public struct Message: Equatable {
metadata: metadata,
jsonData: updatedData ?? jsonData)
}
}

extension Message {
public struct Metadata: Equatable {
public let url: String

/// 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.
/// - 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<T: Encodable>(event updatedEvent: String? = nil,
data: T) -> Message {
let updatedData: String?
do {
let jsonData = try Strada.config.jsonEncoder.encode(data)
updatedData = String(data: jsonData, encoding: .utf8)
} catch {
debugLog("Error encoding codable object: \(data) -> \(error)")
updatedData = nil
}

return replacing(event: updatedEvent, jsonData: updatedData)
}
}

extension Message {
public func decodedJsonData<T: Decodable>() -> 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: Decodable>() -> T? {
guard let data = jsonData.data(using: .utf8) else {
debugLog("Error converting json string to data: \(jsonData)")
return nil
}

do {
let decoder = JsonDataDecoder.appDecoder
let decoder = Strada.config.jsonDecoder
return try decoder.decode(T.self, from: data)
} catch {
debugLog("Error decoding json: \(jsonData) -> \(error)")
return nil
}
}
}

extension Message {
public struct Metadata: Equatable {
public let url: String
}
}
5 changes: 5 additions & 0 deletions Source/Strada.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Foundation

public enum Strada {
public static var config: StradaConfig = StradaConfig()
}
13 changes: 13 additions & 0 deletions Source/StradaConfig.swift
Original file line number Diff line number Diff line change
@@ -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()
}
12 changes: 8 additions & 4 deletions Strada.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 /* JsonDataDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = E200E7D02A814D4500E41FA9 /* JsonDataDecoder.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 */; };
Expand Down Expand Up @@ -59,13 +60,14 @@
C1EB05252588133D00933244 /* MessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageTests.swift; sourceTree = "<group>"; };
C1EB052D2588201600933244 /* BridgeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BridgeTests.swift; sourceTree = "<group>"; };
CBAFC52826F9863900C6662E /* PathLoaderXcode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PathLoaderXcode.swift; sourceTree = "<group>"; };
E200E7D02A814D4500E41FA9 /* JsonDataDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonDataDecoder.swift; sourceTree = "<group>"; };
E20978412A6E9E6B00CDEEE5 /* InternalMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalMessage.swift; sourceTree = "<group>"; };
E20978432A6EAF3600CDEEE5 /* InternalMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalMessageTests.swift; sourceTree = "<group>"; };
E20978462A7135E700CDEEE5 /* Encodable+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Encodable+Utils.swift"; sourceTree = "<group>"; };
E20978482A71366B00CDEEE5 /* Data+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Utils.swift"; sourceTree = "<group>"; };
E209784A2A714D4E00CDEEE5 /* String+JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+JSON.swift"; sourceTree = "<group>"; };
E209784C2A714F1900CDEEE5 /* Dictionary+JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+JSON.swift"; sourceTree = "<group>"; };
E22CBEFE2A84D7060024EFB8 /* StradaConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StradaConfig.swift; sourceTree = "<group>"; };
E22CBF002A84DC380024EFB8 /* Strada.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strada.swift; sourceTree = "<group>"; };
E2DB15902A7163B0001EE08C /* BridgeDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BridgeDelegate.swift; sourceTree = "<group>"; };
E2DB15922A7282CF001EE08C /* BridgeComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BridgeComponent.swift; sourceTree = "<group>"; };
E2DB15942A72B0A8001EE08C /* BridgeDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BridgeDelegateTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -127,7 +129,8 @@
E20978412A6E9E6B00CDEEE5 /* InternalMessage.swift */,
E2DB15902A7163B0001EE08C /* BridgeDelegate.swift */,
E2DB15922A7282CF001EE08C /* BridgeComponent.swift */,
E200E7D02A814D4500E41FA9 /* JsonDataDecoder.swift */,
E22CBEFE2A84D7060024EFB8 /* StradaConfig.swift */,
E22CBF002A84DC380024EFB8 /* Strada.swift */,
);
path = Source;
sourceTree = "<group>";
Expand Down Expand Up @@ -282,12 +285,13 @@
files = (
E209784B2A714D4E00CDEEE5 /* String+JSON.swift in Sources */,
C11349A62587EFFB000A6E56 /* ScriptMessageHandler.swift in Sources */,
E200E7D12A814D4500E41FA9 /* JsonDataDecoder.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 */,
Expand Down
24 changes: 23 additions & 1 deletion Tests/BridgeComponentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,28 @@ class BridgeComponentTest: XCTestCase {
XCTAssertEqual(bridge.replyWithMessageArg, message)
}

func test_replyToReceivedMessageWithACodableObjectSucceeds() {
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", with: messageData)

XCTAssertTrue(success)
XCTAssertTrue(bridge.replyWithMessageWasCalled)
XCTAssertEqual(bridge.replyWithMessageArg, newMessage)
}

func test_replyToMessageNotReceivedWithACodableObjectIgnoresTheReply() {
let messageData = MessageData(title: "hey", subtitle: "", actionName: "tap")

let success = component.reply(to: "disconnect", with: messageData)

XCTAssertFalse(success)
XCTAssertFalse(bridge.replyWithMessageWasCalled)
XCTAssertNil(bridge.replyWithMessageArg)
}

func test_replyToMessageNotReceivedIgnoresTheReply() {
let success = component.reply(to: "disconnect")

Expand All @@ -75,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)
Expand Down
85 changes: 81 additions & 4 deletions Tests/MessageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Strada.config.jsonEncoder = JSONEncoder()
Strada.config.jsonDecoder = JSONDecoder()
}

// MARK: replacing(event:, jsonData:)

func testReplacingWithNewEventAndData() {
let metadata = Message.Metadata(url: "https://37signals.com")
let jsonData = """
Expand Down Expand Up @@ -86,6 +96,49 @@ class MessageTests: XCTestCase {
XCTAssertEqual(newMessage.jsonData, jsonData)
}

// MARK: replacing(event:, data:)

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, data: 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(data: messageData)

XCTAssertEqual(newMessage.id, "1")
XCTAssertEqual(newMessage.component, "page")
XCTAssertEqual(newMessage.event, "connect")
XCTAssertEqual(newMessage.metadata, metadata)
XCTAssertEqual(newMessage.jsonData, newJsonData)
}

// MARK: Decoding

func test_decodingWithDefaultDecoder() {
let metadata = Message.Metadata(url: "https://37signals.com")
let jsonData = """
Expand All @@ -101,17 +154,16 @@ class MessageTests: XCTestCase {
subtitle: "Page-subtitle",
actionName: "go")

let decodedMessageData: MessageData? = message.decodedJsonData()
let decodedMessageData: MessageData? = message.data()

XCTAssertEqual(decodedMessageData, pageData)
}

func test_decodingWithCustomDecoder() {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
JsonDataDecoder.appDecoder = decoder
Strada.config.jsonDecoder = decoder

let metadata = Message.Metadata(url: "https://37signals.com")
let jsonData = """
{"title":"Page-title","subtitle":"Page-subtitle", "action_name": "go"}
"""
Expand All @@ -125,8 +177,33 @@ class MessageTests: XCTestCase {
subtitle: "Page-subtitle",
actionName: "go")

let decodedMessageData: MessageData? = message.decodedJsonData()
let decodedMessageData: MessageData? = message.data()

XCTAssertEqual(decodedMessageData, pageData)
}

// MARK: Custom encoding

func test_encodingWithCustomEncoder() {
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
Strada.config.jsonEncoder = 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(data: messageData)

XCTAssertEqual(message, newMessage)
}
}
2 changes: 1 addition & 1 deletion Tests/Spies/BridgeSpy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 03a77d5

Please sign in to comment.