Skip to content

Commit f3fbf50

Browse files
committed
Support response generic type
1 parent d56ca4d commit f3fbf50

File tree

3 files changed

+240
-0
lines changed

3 files changed

+240
-0
lines changed

Sources/NTLBridge/NTLBridgeUtil.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
import Foundation
22

3+
/// Bridge错误类型
4+
public enum NTLBridgeError: Error, LocalizedError {
5+
case invalidValue
6+
case typeConversionFailed(Error)
7+
8+
public var errorDescription: String? {
9+
switch self {
10+
case .invalidValue:
11+
return "Invalid value for conversion"
12+
case .typeConversionFailed(let error):
13+
return "Type conversion failed: \(error.localizedDescription)"
14+
}
15+
}
16+
}
17+
318
/// Bridge工具类,提供JSON序列化/反序列化功能
419
public final class NTLBridgeUtil {
520

@@ -106,6 +121,37 @@ public final class NTLBridgeUtil {
106121
}
107122
}
108123

124+
// MARK: - Type Conversion
125+
126+
/// 将JSONValue转换为指定类型
127+
/// - Parameter value: 要转换的JSONValue
128+
/// - Returns: 转换后的指定类型,转换失败返回nil
129+
public static func convertValue<T: Decodable>(_ value: JSONValue?) -> T? {
130+
guard let value = value else { return nil }
131+
132+
do {
133+
let data = try encoder.encode(value)
134+
return try decoder.decode(T.self, from: data)
135+
} catch {
136+
return nil
137+
}
138+
}
139+
140+
/// 将JSONValue转换为指定类型,抛出错误
141+
/// - Parameter value: 要转换的JSONValue
142+
/// - Returns: 转换后的指定类型
143+
/// - Throws: 类型转换错误
144+
public static func convertValueOrThrow<T: Decodable>(_ value: JSONValue?) throws -> T {
145+
guard let value = value else { throw NTLBridgeError.invalidValue }
146+
147+
do {
148+
let data = try encoder.encode(value)
149+
return try decoder.decode(T.self, from: data)
150+
} catch {
151+
throw NTLBridgeError.typeConversionFailed(error)
152+
}
153+
}
154+
109155
// MARK: - Validation
110156

111157
/// 验证方法名是否有效

Sources/NTLBridge/NTLWebView.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,33 @@ open class NTLWebView: WKWebView {
288288
}
289289
}
290290

291+
/// 调用 js bridge 方法并返回指定类型
292+
/// - Parameters:
293+
/// - method: JavaScript注册方法名,比如 "nameA.funcB"
294+
/// - args: 参数数组
295+
/// - completion: 完成回调,返回指定类型的结果
296+
/// - discussion: js 端目前 async 只用 callback 来注册回调。参数长度要固定。不支持 Promise。
297+
public func callBridge<T: Decodable>(
298+
method: String,
299+
args: [JSONValue] = [],
300+
completion: @escaping (Result<T, Error>) -> Void
301+
) {
302+
// 调用原来的函数,在回调中进行类型转换
303+
callBridge(method: method, args: args) { result in
304+
switch result {
305+
case .success(let jsonValue):
306+
do {
307+
let typedValue: T = try NTLBridgeUtil.convertValueOrThrow(jsonValue)
308+
completion(.success(typedValue))
309+
} catch {
310+
completion(.failure(error))
311+
}
312+
case .failure(let error):
313+
completion(.failure(error))
314+
}
315+
}
316+
}
317+
291318
// MARK: - Private Methods
292319

293320
/// 内部使用的注册方法,跳过验证

Tests/NTLBridgeTests/NTLWebViewTests.swift

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,173 @@ struct NTLWebViewTests {
357357
}
358358
}
359359

360+
// MARK: - Generic Call Bridge Tests
361+
362+
@Test("Generic call bridge with string return")
363+
func genericCallBridgeWithStringReturn() async {
364+
await MainActor.run {
365+
let webView = NTLWebView()
366+
367+
var result: String?
368+
var error: Error?
369+
370+
webView.callBridge(method: "testStringMethod", args: []) { (response: Result<String, Error>) in
371+
switch response {
372+
case .success(let value):
373+
result = value
374+
case .failure(let err):
375+
error = err
376+
}
377+
}
378+
379+
// Simulate a successful JS return by directly calling handleReturnValueFromJS
380+
let mockResponse: JSONValue = .dictionary([
381+
"id": .number(1),
382+
"data": .string("Hello from JavaScript!"),
383+
"complete": .bool(true)
384+
])
385+
webView.handleReturnValueFromJS(mockResponse)
386+
387+
// Verify the successful type conversion
388+
#expect(result == "Hello from JavaScript!")
389+
#expect(error == nil)
390+
}
391+
}
392+
393+
@Test("Generic call bridge with custom struct")
394+
func genericCallBridgeWithCustomStruct() async {
395+
await MainActor.run {
396+
let webView = NTLWebView()
397+
398+
struct TestUser: Codable, Equatable {
399+
let name: String
400+
let age: Int
401+
}
402+
403+
var result: TestUser?
404+
var error: Error?
405+
406+
webView.callBridge(method: "testUserMethod", args: []) { (response: Result<TestUser, Error>) in
407+
switch response {
408+
case .success(let value):
409+
result = value
410+
case .failure(let err):
411+
error = err
412+
}
413+
}
414+
415+
// Simulate a successful JS return with user data
416+
let mockResponse: JSONValue = .dictionary([
417+
"id": .number(1),
418+
"data": .dictionary([
419+
"name": .string("Alice Johnson"),
420+
"age": .number(28)
421+
]),
422+
"complete": .bool(true)
423+
])
424+
webView.handleReturnValueFromJS(mockResponse)
425+
426+
// Verify the successful type conversion
427+
let expectedUser = TestUser(name: "Alice Johnson", age: 28)
428+
#expect(result == expectedUser)
429+
#expect(error == nil)
430+
}
431+
}
432+
433+
@Test("Generic call bridge with array return")
434+
func genericCallBridgeWithArrayReturn() async {
435+
await MainActor.run {
436+
let webView = NTLWebView()
437+
438+
struct TestItem: Codable, Equatable {
439+
let id: Int
440+
let title: String
441+
}
442+
443+
var result: [TestItem]?
444+
var error: Error?
445+
446+
webView.callBridge(method: "testArrayMethod", args: []) { (response: Result<[TestItem], Error>) in
447+
switch response {
448+
case .success(let value):
449+
result = value
450+
case .failure(let err):
451+
error = err
452+
}
453+
}
454+
455+
// Simulate a successful JS return with array data
456+
let mockResponse: JSONValue = .dictionary([
457+
"id": .number(1),
458+
"data": .array([
459+
.dictionary(["id": .number(1), "title": .string("First Item")]),
460+
.dictionary(["id": .number(2), "title": .string("Second Item")]),
461+
.dictionary(["id": .number(3), "title": .string("Third Item")])
462+
]),
463+
"complete": .bool(true)
464+
])
465+
webView.handleReturnValueFromJS(mockResponse)
466+
467+
// Verify the successful type conversion
468+
let expectedItems = [
469+
TestItem(id: 1, title: "First Item"),
470+
TestItem(id: 2, title: "Second Item"),
471+
TestItem(id: 3, title: "Third Item")
472+
]
473+
#expect(result == expectedItems)
474+
#expect(error == nil)
475+
}
476+
}
477+
478+
@Test("Generic call bridge with type conversion failure")
479+
func genericCallBridgeWithTypeConversionFailure() async {
480+
await MainActor.run {
481+
let webView = NTLWebView()
482+
483+
struct StrictUser: Codable, Equatable {
484+
let name: String
485+
let age: Int
486+
let email: String // Required field
487+
}
488+
489+
var result: StrictUser?
490+
var error: Error?
491+
492+
webView.callBridge(method: "testIncompleteUserMethod", args: []) { (response: Result<StrictUser, Error>) in
493+
switch response {
494+
case .success(let value):
495+
result = value
496+
case .failure(let err):
497+
error = err
498+
}
499+
}
500+
501+
// Simulate JS return with incomplete data (missing required 'email' field)
502+
let mockResponse: JSONValue = .dictionary([
503+
"id": .number(1),
504+
"data": .dictionary([
505+
"name": .string("Bob Smith"),
506+
"age": .number(35)
507+
// Missing 'email' field which is required
508+
]),
509+
"complete": .bool(true)
510+
])
511+
webView.handleReturnValueFromJS(mockResponse)
512+
513+
// Verify the type conversion failed as expected
514+
#expect(result == nil)
515+
#expect(error != nil)
516+
517+
// Verify it's a type conversion error
518+
#expect(error is NTLBridgeError)
519+
if case .typeConversionFailed = error as? NTLBridgeError {
520+
// Expected error type
521+
} else {
522+
#expect(false) // Unexpected error type
523+
}
524+
}
525+
}
526+
360527
// MARK: - Navigation Tests
361528

362529
@Test("Load request triggers cleanup")

0 commit comments

Comments
 (0)