Skip to content

Commit efe41ca

Browse files
committed
public deserializers
1 parent 0dcb35e commit efe41ca

File tree

5 files changed

+205
-55
lines changed

5 files changed

+205
-55
lines changed

Source/Content/DataContent.swift

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,43 @@
11
import Foundation
22

3-
/// A deserializer that extracts raw `Data` from a `SmartResponse`.
3+
/// A deserializer that extracts the raw `Data` payload from a `SmartResponse`.
44
///
5-
/// This is used when the expected output is unprocessed binary data. If the response
6-
/// includes an error or is missing a body, decoding will fail.
7-
struct DataContent: Deserializable {
8-
/// Attempts to extract the raw body data from a `SmartResponse`.
5+
/// Use `DataContent` when you expect unprocessed binary data (for example, images,
6+
/// files, or any opaque blob returned by the server). If the response contains a
7+
/// non-`nil` `error`, the decode result is `.failure(error)`. If the response has
8+
/// no body, the result is `.failure(RequestDecodingError.nilResponse)`.
9+
///
10+
/// This type is stateless and thread‑safe.
11+
///
12+
/// - SeeAlso: `Deserializable`, `SmartResponse`, `RequestDecodingError`
13+
public struct DataContent: Deserializable {
14+
/// Creates a new `DataContent` deserializer.
15+
public init() {}
16+
17+
/// Attempts to extract the raw body `Data` from a `SmartResponse`.
18+
///
19+
/// Error precedence is as follows:
20+
/// 1. If `data.error` is non-`nil`, return `.failure` with that error.
21+
/// 2. If `data.body` is `nil`, return `.failure(RequestDecodingError.nilResponse)`.
22+
/// 3. Otherwise, return `.success(Data)` with the body bytes.
923
///
1024
/// - Parameters:
11-
/// - data: The response to decode, including body and error.
12-
/// - parameters: Request parameters (unused here).
13-
/// - Returns: A result containing the response body as `Data`, or an error if unavailable.
14-
func decode(with data: SmartResponse, parameters: Parameters) -> Result<Data, Error> {
25+
/// - data: The response to decode, containing `body` and `error`.
26+
/// - parameters: Request parameters. Not used by this type.
27+
/// - Returns: `.success` with the response body as `Data`, or `.failure` describing why the body is unavailable.
28+
///
29+
/// # Example
30+
/// ```swift
31+
/// let deserializer = DataContent()
32+
/// let result = deserializer.decode(with: response, parameters: [:])
33+
/// switch result {
34+
/// case .success(let payload):
35+
/// print("bytes:", payload.count)
36+
/// case .failure(let error):
37+
/// print("decode failed:", error)
38+
/// }
39+
/// ```
40+
public func decode(with data: SmartResponse, parameters: Parameters) -> Result<Data, Error> {
1541
if let error = data.error {
1642
return .failure(error)
1743
} else if let data = data.body {

Source/Content/DecodableContent.swift

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,67 @@
11
import Foundation
22

3-
/// A deserializer that decodes a `SmartResponse` body into a `Decodable` type.
3+
/// A deserializer that decodes a `SmartResponse` body into a concrete `Decodable` model.
44
///
5-
/// Supports decoding with optional key paths for nested JSON structures and fallback behavior
6-
/// if decoding fails. Uses a provided or default `JSONDecoder`.
5+
/// `DecodableContent` supports:
6+
/// - Decoding the top-level body or a nested value via a JSON key path.
7+
/// - Supplying a custom `JSONDecoder` (via a closure) or using a default one.
8+
/// - Fallback behavior when decoding fails: return a specific error, a default value, or the thrown decoding error.
79
///
8-
struct DecodableContent<Response: Decodable>: Deserializable {
9-
/// An optional closure that returns a `JSONDecoder` for decoding.
10+
/// The instance is value-typed and thread-safe as long as the provided `decoder` closure is pure and thread-safe.
11+
///
12+
/// - SeeAlso: `DecodableKeyPath`, `SmartResponse`, `RequestDecodingError`
13+
public struct DecodableContent<Response: Decodable>: Deserializable {
14+
/// Optional factory for the `JSONDecoder` used during decoding.
15+
///
16+
/// If `nil`, a fresh `JSONDecoder()` is constructed for each decode. Provide a closure
17+
/// if you need custom strategies (dates, keys, data) or performance optimizations via
18+
/// a reused decoder instance.
19+
public let decoder: JSONDecoding?
20+
/// Describes the JSON key path of the target value and the fallback strategy when decoding fails.
1021
///
11-
/// If `nil`, a default `JSONDecoder` is used.
12-
let decoder: JSONDecoding?
13-
/// Defines the key path to the target value within the response body and fallback behavior.
14-
let keyPath: DecodableKeyPath<Response>
22+
/// Use an empty path to decode the top-level body. Non-empty paths navigate into nested JSON.
23+
public let keyPath: DecodableKeyPath<Response>
24+
25+
/// Creates a `DecodableContent` deserializer.
26+
/// - Parameters:
27+
/// - decoder: Optional closure that supplies a `JSONDecoder` per decode call.
28+
/// - keyPath: Key-path descriptor and fallback policy for decoding.
29+
public init(decoder: JSONDecoding?, keyPath: DecodableKeyPath<Response>) {
30+
self.decoder = decoder
31+
self.keyPath = keyPath
32+
}
1533

16-
/// Attempts to decode the response body into the expected type using a decoder and optional key path.
34+
/// Attempts to decode the `SmartResponse` body into `Response` using the configured decoder and key path.
35+
///
36+
/// Error precedence is as follows:
37+
/// 1. If `data.error` is non-`nil`, return `.failure` with that error.
38+
/// 2. If `data.body` is `nil`, return `.failure(RequestDecodingError.nilResponse)`.
39+
/// 3. If `data.body` is empty, return `.failure(RequestDecodingError.emptyResponse)`.
40+
/// 4. If JSON decoding throws, apply `keyPath.fallback`:
41+
/// - `.error(e)`: return `.failure(e)`
42+
/// - `.value(v)`: return `.success(v)`
43+
/// - `.none`: return `.failure` with the thrown decoding error
1744
///
1845
/// - Parameters:
19-
/// - data: The response containing the body and any error.
20-
/// - parameters: Request parameters (unused in this context).
21-
/// - Returns: A result containing either the decoded value or an error.
22-
func decode(with data: SmartResponse, parameters: Parameters) -> Result<Response, Error> {
46+
/// - data: The response containing the raw body bytes and/or an error.
47+
/// - parameters: Request parameters. Unused by this type.
48+
/// - Returns: `.success` with a decoded `Response` or `.failure` describing why decoding was not possible.
49+
///
50+
/// # Example
51+
/// Decode top-level JSON:
52+
/// ```swift
53+
/// struct User: Decodable { let id: Int; let name: String }
54+
/// let content = DecodableContent<User>(decoder: nil, keyPath: .root())
55+
/// let result = content.decode(with: response, parameters: [:])
56+
/// ```
57+
///
58+
/// Decode a nested field using a key path and default value on failure:
59+
/// ```swift
60+
/// let kp = DecodableKeyPath<[User]>(path: ["data", "users"], fallback: .value([]))
61+
/// let content = DecodableContent<[User]>(decoder: { JSONDecoder() }, keyPath: kp)
62+
/// let result = content.decode(with: response, parameters: [:])
63+
/// ```
64+
public func decode(with data: SmartResponse, parameters: Parameters) -> Result<Response, Error> {
2365
if let error = data.error {
2466
return .failure(error)
2567
} else if let data = data.body {
@@ -30,11 +72,11 @@ struct DecodableContent<Response: Decodable>: Deserializable {
3072
do {
3173
let decoder = decoder?() ?? .init()
3274
let result: Response =
33-
if keyPath.path.isEmpty {
34-
try decoder.decode(Response.self, from: data)
35-
} else {
36-
try data.decode(Response.self, keyPath: keyPath.path, decoder: decoder)
37-
}
75+
if keyPath.path.isEmpty {
76+
try decoder.decode(Response.self, from: data)
77+
} else {
78+
try data.decode(Response.self, keyPath: keyPath.path, decoder: decoder)
79+
}
3880
return .success(result)
3981
} catch {
4082
switch keyPath.fallback {

Source/Content/ImageContent.swift

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,45 @@
11
import Foundation
22

3-
/// A deserializer that converts response data into a platform-specific image.
3+
/// A deserializer that converts a `SmartResponse` body into a platform-specific image (`SmartImage`).
44
///
5-
/// This is useful for decoding image responses from the network into `SmartImage`
6-
/// (e.g., `UIImage` on iOS or `NSImage` on macOS).
7-
struct ImageContent: Deserializable {
8-
/// Attempts to decode image data from the response.
5+
/// Use `ImageContent` for endpoints that return raw image bytes (PNG/JPEG/HEIF/WebP, etc.).
6+
/// The type attempts to create a `PlatformImage` from the raw bytes and expose its SDK image via `SmartImage`.
7+
/// If `data.error` is non-`nil`, that error is returned. If the body is `nil` or empty, a
8+
/// corresponding `RequestDecodingError` is returned. If image creation fails, `.brokenImage` is returned.
9+
///
10+
/// This type is stateless and thread‑safe.
11+
///
12+
/// - SeeAlso: `SmartImage`, `PlatformImage`, `RequestDecodingError`, `DataContent`
13+
public struct ImageContent: Deserializable {
14+
/// Creates a new `ImageContent` deserializer.
15+
public init() {}
16+
17+
/// Attempts to decode an image from the `SmartResponse` body.
18+
///
19+
/// Error precedence is as follows:
20+
/// 1. If `data.error` is non‑`nil`, return `.failure` with that error.
21+
/// 2. If `data.body` is `nil`, return `.failure(RequestDecodingError.nilResponse)`.
22+
/// 3. If `data.body` is empty, return `.failure(RequestDecodingError.emptyResponse)`.
23+
/// 4. If image construction fails, return `.failure(RequestDecodingError.brokenImage)`.
924
///
1025
/// - Parameters:
11-
/// - data: The response object containing the body and any associated error.
12-
/// - parameters: The request parameters (unused).
13-
/// - Returns: A result containing a decoded image or a decoding error.
14-
func decode(with data: SmartResponse, parameters: Parameters) -> Result<SmartImage, Error> {
26+
/// - data: The response object containing the raw body and any associated error.
27+
/// - parameters: Request parameters. Not used by this type.
28+
/// - Returns: `.success(SmartImage)` if decoding succeeds, otherwise `.failure` with the reason.
29+
///
30+
/// # Example
31+
/// ```swift
32+
/// let content = ImageContent()
33+
/// let result = content.decode(with: response, parameters: [:])
34+
/// switch result {
35+
/// case .success(let image):
36+
/// // use image
37+
/// _ = image
38+
/// case .failure(let error):
39+
/// print("image decode failed:", error)
40+
/// }
41+
/// ```
42+
public func decode(with data: SmartResponse, parameters: Parameters) -> Result<SmartImage, Error> {
1543
if let error = data.error {
1644
return .failure(error)
1745
} else if let data = data.body {

Source/Content/JSONContent.swift

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,54 @@
11
import Foundation
22

3-
/// A deserializer that interprets response data as a JSON object.
3+
/// A deserializer that interprets a `SmartResponse` body as a JSON value using `JSONSerialization`.
44
///
5-
/// Attempts to convert the response body into a Foundation-compatible `Any` type using `JSONSerialization`.
6-
/// Suitable for endpoints that return unstructured or dynamic JSON content.
5+
/// `JSONContent` is suitable for endpoints that return dynamic or loosely-typed JSON where
6+
/// a concrete `Decodable` model is not available. It produces Foundation-compatible values
7+
/// (`Array`, `Dictionary`, `String`, `NSNumber`, `NSNull`).
78
///
8-
struct JSONContent: Deserializable {
9-
/// Decodes the response body into a Foundation JSON object.
9+
/// If `data.error` is non-`nil`, that error is returned. If the body is `nil` or empty,
10+
/// the corresponding `RequestDecodingError` is returned. On JSON parsing failure, the
11+
/// thrown `JSONSerialization` error is returned.
12+
///
13+
/// This type is stateless and thread-safe.
14+
///
15+
/// - SeeAlso: `DecodableContent`, `DataContent`, `SmartResponse`, `RequestDecodingError`
16+
public struct JSONContent: Deserializable {
17+
/// Creates a new `JSONContent` deserializer.
18+
public init() {}
19+
20+
/// Decodes the response body into a Foundation JSON value using `JSONSerialization`.
21+
///
22+
/// Error precedence is as follows:
23+
/// 1. If `data.error` is non-`nil`, return `.failure` with that error.
24+
/// 2. If `data.body` is `nil`, return `.failure(RequestDecodingError.nilResponse)`.
25+
/// 3. If `data.body` is empty, return `.failure(RequestDecodingError.emptyResponse)`.
26+
/// 4. If JSON parsing throws, return `.failure` with the thrown error.
1027
///
1128
/// - Parameters:
12-
/// - data: The `SmartResponse` containing the body and any error.
13-
/// - parameters: The request parameters (unused).
14-
/// - Returns: A `.success` result containing the parsed JSON object or a `.failure` with an appropriate decoding error.
15-
func decode(with data: SmartResponse, parameters: Parameters) -> Result<Any, Error> {
29+
/// - data: The `SmartResponse` containing the raw body and any associated error.
30+
/// - parameters: Request parameters. Not used by this type.
31+
/// - Returns: `.success(Any)` with a Foundation JSON value or `.failure` describing why decoding was not possible.
32+
///
33+
/// # Example
34+
/// ```swift
35+
/// let content = JSONContent()
36+
/// let result = content.decode(with: response, parameters: [:])
37+
/// switch result {
38+
/// case .success(let json):
39+
/// if let dict = json as? [String: Any] { print(dict) }
40+
/// case .failure(let error):
41+
/// print("json decode failed:", error)
42+
/// }
43+
/// ```
44+
public func decode(with data: SmartResponse, parameters: Parameters) -> Result<Any, Error> {
1645
if let error = data.error {
1746
return .failure(error)
1847
} else if let data = data.body {
1948
if data.isEmpty {
2049
return .failure(RequestDecodingError.emptyResponse)
2150
}
22-
51+
2352
do {
2453
return try .success(JSONSerialization.jsonObject(with: data))
2554
} catch {

Source/Content/VoidContent.swift

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,42 @@
11
import Foundation
22

3-
/// A deserializer used for requests that expect no response body.
3+
/// A deserializer for requests that are expected to have no response body.
44
///
5-
/// `VoidContent` returns `.success(())` on success and propagates errors if present,
6-
/// ignoring the response body entirely. It also treats HTTP 204 (No Content) as success.
7-
struct VoidContent: Deserializable {
8-
/// Interprets a `SmartResponse` for use cases where no body is expected.
5+
/// `VoidContent` ignores any response payload and reports success when the transport
6+
/// layer indicates success. It treats HTTP 204 (No Content) as success even when
7+
/// surfaced as a `.noContent` status error in `SmartResponse.error`.
8+
///
9+
/// This type is stateless and thread-safe.
10+
///
11+
/// - SeeAlso: `SmartResponse`, `StatusCode.noContent`, `RequestDecodingError`
12+
public struct VoidContent: Deserializable {
13+
/// Creates a new `VoidContent` deserializer.
14+
public init() {}
15+
16+
/// Interprets a `SmartResponse` when no body is expected.
17+
///
18+
/// Error precedence is as follows:
19+
/// 1. If `data.error` is `.noContent` (HTTP 204), return `.success(())`.
20+
/// 2. If `data.error` is any other non-`nil` error, return `.failure` with that error.
21+
/// 3. Otherwise, return `.success(())` regardless of `data.body`.
922
///
1023
/// - Parameters:
1124
/// - data: The response to evaluate.
12-
/// - parameters: The request parameters (not used).
13-
/// - Returns: `.success(())` if the response has no error or if the error is `.noContent`; otherwise returns `.failure`.
14-
func decode(with data: SmartResponse, parameters: Parameters) -> Result<Void, Error> {
25+
/// - parameters: Request parameters. Not used by this type.
26+
/// - Returns: `.success(())` when there is no error or when the error is `.noContent`; otherwise `.failure` with the error.
27+
///
28+
/// # Example
29+
/// ```swift
30+
/// let content = VoidContent()
31+
/// let result = content.decode(with: response, parameters: [:])
32+
/// switch result {
33+
/// case .success:
34+
/// print("no content as expected")
35+
/// case .failure(let error):
36+
/// print("request failed:", error)
37+
/// }
38+
/// ```
39+
public func decode(with data: SmartResponse, parameters: Parameters) -> Result<Void, Error> {
1540
if let error = data.error {
1641
if let error = data.error as? StatusCode, error == .noContent {
1742
return .success(())

0 commit comments

Comments
 (0)