Skip to content

Custom decoder for RestClient #640

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

Open
wants to merge 1 commit into
base: SDKS-9513_2
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion Split/Api/SplitClientManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ class DefaultClientManager: SplitClientManager {
return
}
Logger.v("Cache validated; starting sync manager")
syncManager.start()
self.syncManager.start()
}
}

Expand Down
12 changes: 12 additions & 0 deletions Split/Network/DataParsing/Json.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ struct Json {
return try Self.decodeFrom(json: data, to: type)
}

/// Decode using a custom decoder function
/// - Parameters:
/// - decoder: A function that takes Data and returns a decoded object of type T
/// - Returns: The decoded object
/// - Throws: Decoding errors if the JSON cannot be parsed
func decodeWith<T>(_ decoder: (Data) throws -> T) throws -> T? {
guard let data = data else {
return nil
}
return try decoder(data)
}

func dynamicDecode<T>(_ type: T.Type) throws -> T? where T: DynamicDecodable {
var obj: T?
if let data = self.data {
Expand Down
15 changes: 13 additions & 2 deletions Split/Network/RestClient/RestClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class DefaultRestClient: SplitApiRestClient {
parameters: HttpParameters? = nil,
body: Data? = nil,
headers: HttpHeaders? = nil,
customDecoder: ((Data) throws -> T)? = nil,
completion: @escaping (DataResult<T>) -> Void) where T: Decodable {

do {
Expand All @@ -73,8 +74,18 @@ class DefaultRestClient: SplitApiRestClient {
}

do {
let parsedObject = try json.decode(T.self)
completion(DataResult { return parsedObject })
if let customDecoder = customDecoder {
// Use the custom decoder if provided
if let parsedObject = try json.decodeWith(customDecoder) {
completion(DataResult { return parsedObject })
} else {
completion(DataResult { return nil })
}
} else {
// Use the default decoder
let parsedObject = try json.decode(T.self)
completion(DataResult { return parsedObject })
}
} catch {
completion(DataResult { throw error })
}
Expand Down
137 changes: 137 additions & 0 deletions SplitTests/RestClientCustomDecoderTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
//
// RestClientCustomDecoderTest.swift
// SplitTests
//
// Created on 13/05/2025.
// Copyright © 2025 Split. All rights reserved.
//

import XCTest
@testable import Split

class RestClientCustomDecoderTest: XCTestCase {

private var httpSession: HttpSessionMock!
private var requestManager: HttpRequestManagerMock!
private var restClient: DefaultRestClient!

override func setUp() {
super.setUp()
httpSession = HttpSessionMock()
requestManager = HttpRequestManagerMock()
let serviceEndpoints = ServiceEndpoints.builder()
.set(sdkEndpoint: "https://sdk.split-test.io")
.set(eventsEndpoint: "https://events.split-test.io").build()
let endpointFactory = EndpointFactory(serviceEndpoints: serviceEndpoints, apiKey: "dummy-key", splitsQueryString: "")
let httpClient = DefaultHttpClient(session: httpSession, requestManager: requestManager)
restClient = DefaultRestClient(httpClient: httpClient, endpointFactory: endpointFactory)
}

override func tearDown() {
httpSession = nil
requestManager = nil
restClient = nil
super.tearDown()
}

func testExecuteWithDefaultDecoder() {
let json = """
{
"id": 123,
"name": "test"
}
"""

let dummyData = Data(json.utf8)
let expectation = XCTestExpectation(description: "API call completes")
var result: TestModel?
var error: Error?

restClient.execute(
endpoint: restClient.endpointFactory.splitChangesEndpoint,
parameters: nil,
headers: nil,
completion: { (dataResult: DataResult<TestModel>) in
do {
result = try dataResult.unwrap()
expectation.fulfill()
} catch let err {
error = err
expectation.fulfill()
}
})

requestManager.append(data: dummyData, to: 1)
_ = requestManager.set(responseCode: 200, to: 1)

wait(for: [expectation], timeout: 1)

XCTAssertEqual(1, httpSession.dataTaskCallCount)
XCTAssertEqual(1, requestManager.addRequestCallCount)
XCTAssertNil(error)
XCTAssertNotNil(result)
XCTAssertEqual(result?.id, 123)
XCTAssertEqual(result?.name, "test")
}

func testExecuteWithCustomDecoder() {
let json = """
{
"custom_id": 456,
"custom_name": "custom_test"
}
"""

let dummyData = Data(json.utf8)
let expectation = XCTestExpectation(description: "API call completes")
let customDecoderCalled = XCTestExpectation(description: "Custom decoder called")
var result: TestModel?
var error: Error?

let customDecoder: (Data) throws -> TestModel = { data in
customDecoderCalled.fulfill()

let decoder = JSONDecoder()
let customModel = try decoder.decode(CustomTestModel.self, from: data)
// Convert from custom model to standard model
return TestModel(id: customModel.custom_id, name: customModel.custom_name)
}

restClient.execute(
endpoint: restClient.endpointFactory.splitChangesEndpoint,
parameters: nil,
headers: nil,
customDecoder: customDecoder,
completion: { (dataResult: DataResult<TestModel>) in
do {
result = try dataResult.unwrap()
expectation.fulfill()
} catch let err {
error = err
expectation.fulfill()
}
})

requestManager.append(data: dummyData, to: 1)
_ = requestManager.set(responseCode: 200, to: 1)

wait(for: [expectation, customDecoderCalled], timeout: 1)

XCTAssertEqual(1, httpSession.dataTaskCallCount)
XCTAssertEqual(1, requestManager.addRequestCallCount)
XCTAssertNil(error)
XCTAssertNotNil(result)
XCTAssertEqual(result?.id, 456)
XCTAssertEqual(result?.name, "custom_test")
}
}

private struct TestModel: Decodable {
let id: Int
let name: String
}

private struct CustomTestModel: Decodable {
let custom_id: Int
let custom_name: String
}