Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reply with an Encodable type #12

Merged
merged 7 commits into from
Aug 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
jayohms marked this conversation as resolved.
Show resolved Hide resolved
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()
jayohms marked this conversation as resolved.
Show resolved Hide resolved
}
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