api-swift / Sources / IrdAPI / Core / RequestBuilder.swift
391 words
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import Foundation

/// URL and header construction, byte-compatible with the sibling clients.
enum RequestBuilder {
    private static let unreserved: Set<UInt8> = {
        var s = Set<UInt8>()
        for c in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~".utf8 { s.insert(c) }
        return s
    }()

    /// Percent-encodes one component like `encodeURIComponent` plus `!'()*`: `%20` for
    /// spaces (never `+`), uppercase hex, `/` encoded so a value stays one path segment.
    static func encode(_ value: String) -> String {
        var out = ""
        out.reserveCapacity(value.utf8.count)
        let hex = Array("0123456789ABCDEF")
        for byte in value.utf8 {
            if unreserved.contains(byte) {
                out.append(Character(UnicodeScalar(byte)))
            } else {
                out.append("%")
                out.append(hex[Int(byte >> 4)])
                out.append(hex[Int(byte & 0x0F)])
            }
        }
        return out
    }

    /// `?a=b&c=d` in the given order, or "" when there are no items.
    static func queryString(_ items: [QueryItem]) -> String {
        if items.isEmpty { return "" }
        return "?" + items.map { "\(encode($0.name))=\(encode($0.value))" }.joined(separator: "&")
    }

    /// Fills `{name}` placeholders from `params`, each value encoded as a single segment.
    static func fillPath(_ template: String, params: [String: String]) throws -> String {
        var out = ""
        var rest = Substring(template)
        while let open = rest.firstIndex(of: "{") {
            out += rest[..<open]
            guard let close = rest[open...].firstIndex(of: "}") else { throw IrdError.config("Unbalanced path template \(template)") }
            let name = String(rest[rest.index(after: open)..<close])
            guard let value = params[name] else {
                throw IrdError.config("Missing path parameter {\(name)} for \(template)")
            }
            out += encode(value)
            rest = rest[rest.index(after: close)...]
        }
        out += rest
        return out
    }

    /// The base URL with trailing slashes stripped.
    static func base(_ url: URL) -> String {
        var s = url.absoluteString
        while s.hasSuffix("/") { s.removeLast() }
        return s
    }

    /// Assembles the request: `Accept: application/json`, the bearer when a non-blank token
    /// is given, `Content-Type: application/json` only when a body exists, then caller
    /// headers on top.
    static func build(url: URL, method: HTTPMethod, token: String?, body: Data?, contentType: String = "application/json",
                      headers: [String: String], timeout: TimeInterval?) -> URLRequest {
        var r = timeout.map { URLRequest(url: url, timeoutInterval: $0) } ?? URLRequest(url: url)
        r.httpMethod = method.rawValue
        r.setValue("application/json", forHTTPHeaderField: "Accept")
        if let token, !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
            r.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        }
        if let body {
            r.setValue(contentType, forHTTPHeaderField: "Content-Type")
            r.httpBody = body
        }
        for (k, v) in headers { r.setValue(v, forHTTPHeaderField: k) }
        return r
    }
}

/// Wraps an existential so `JSONEncoder` can encode it.
struct AnyEncodable: Encodable {
    let value: any Encodable
    func encode(to encoder: any Encoder) throws { try value.encode(to: encoder) }
}

static mirror of HEAD · about · clone: git clone https://ird.cod-sursa.ro/api-swift.git