- Core: Schema (Codable), Store, EventBus, RPC, Engine, I18n - Controls: Text, Title1-6, Label, Input, Textarea, Number, Date, Button, Select VBox, HBox, ScrollPanel, DynamicColumn, Tabular, Form, InlineForm, TabView, Menu, PopupWindow, Html, Image, UrlWidget - Renderer: BricksView (main entry), ControlRenderer (recursive dispatch) - 30+ widget types, 9 bind actiontypes - Form validation (required/minlength/maxlength/min/max/email/number/pattern) - i18n with multi-language JSON files - 18 tests covering schema parsing, store, i18n, events
183 lines
5.8 KiB
Swift
183 lines
5.8 KiB
Swift
import Foundation
|
||
|
||
/// RPC网络层 — 对应Bricks的bricks.tget()和fetch
|
||
/// 处理API调用、数据获取、文件上传
|
||
@MainActor
|
||
public final class BricksRPC: ObservableObject {
|
||
|
||
public struct Response: Sendable {
|
||
public let data: Data
|
||
public let statusCode: Int
|
||
public let headers: [String: String]
|
||
|
||
public var json: Any? {
|
||
try? JSONSerialization.jsonObject(with: data)
|
||
}
|
||
|
||
public var dictionary: [String: Any]? {
|
||
json as? [String: Any]
|
||
}
|
||
|
||
public var array: [[String: Any]]? {
|
||
if let dict = dictionary, let rows = dict["rows"] as? [[String: Any]] {
|
||
return rows
|
||
}
|
||
return json as? [[String: Any]]
|
||
}
|
||
|
||
public var text: String {
|
||
String(data: data, encoding: .utf8) ?? ""
|
||
}
|
||
|
||
public var isSuccess: Bool {
|
||
(200...299).contains(statusCode)
|
||
}
|
||
}
|
||
|
||
/// 基础URL(可选,用于相对路径解析)
|
||
public var baseURL: String = ""
|
||
|
||
/// 默认请求头
|
||
public var defaultHeaders: [String: String] = [
|
||
"Content-Type": "application/json"
|
||
]
|
||
|
||
/// 认证token
|
||
public var authToken: String?
|
||
|
||
/// 请求拦截器
|
||
public var requestInterceptor: ((URLRequest) -> URLRequest)?
|
||
|
||
/// 活跃请求数
|
||
@Published public var activeRequests: Int = 0
|
||
|
||
private let session: URLSession
|
||
|
||
public init(session: URLSession = .shared) {
|
||
self.session = session
|
||
}
|
||
|
||
// MARK: - GET
|
||
|
||
public func get(_ url: String, params: [String: String]? = nil) async throws -> Response {
|
||
var urlString = resolveURL(url)
|
||
if let params, !params.isEmpty {
|
||
let query = params.map { "\($0.key)=\($0.value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? $0.value)" }
|
||
.joined(separator: "&")
|
||
urlString += (urlString.contains("?") ? "&" : "?") + query
|
||
}
|
||
return try await request(urlString, method: "GET")
|
||
}
|
||
|
||
// MARK: - POST
|
||
|
||
public func post(_ url: String, body: [String: Any]? = nil) async throws -> Response {
|
||
var data: Data?
|
||
if let body {
|
||
data = try JSONSerialization.data(withJSONObject: body)
|
||
}
|
||
return try await request(resolveURL(url), method: "POST", body: data)
|
||
}
|
||
|
||
public func postForm(_ url: String, fields: [String: String]) async throws -> Response {
|
||
let formString = fields.map { "\($0.key)=\($0.value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? $0.value)" }
|
||
.joined(separator: "&")
|
||
let data = formString.data(using: .utf8)
|
||
return try await request(resolveURL(url), method: "POST", body: data,
|
||
headers: ["Content-Type": "application/x-www-form-urlencoded"])
|
||
}
|
||
|
||
// MARK: - 通用请求
|
||
|
||
public func request(_ url: String, method: String = "GET", body: Data? = nil,
|
||
headers: [String: String]? = nil) async throws -> Response {
|
||
activeRequests += 1
|
||
defer { activeRequests -= 1 }
|
||
|
||
guard let urlObj = URL(string: url) else {
|
||
throw BricksRPCError.invalidURL(url)
|
||
}
|
||
|
||
var request = URLRequest(url: urlObj)
|
||
request.httpMethod = method
|
||
request.httpBody = body
|
||
request.timeoutInterval = 30
|
||
|
||
// 设置默认头
|
||
for (key, value) in defaultHeaders {
|
||
request.setValue(value, forHTTPHeaderField: key)
|
||
}
|
||
|
||
// 自定义头
|
||
if let headers {
|
||
for (key, value) in headers {
|
||
request.setValue(value, forHTTPHeaderField: key)
|
||
}
|
||
}
|
||
|
||
// 认证
|
||
if let token = authToken {
|
||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||
}
|
||
|
||
// 拦截器
|
||
if let interceptor = requestInterceptor {
|
||
request = interceptor(request)
|
||
}
|
||
|
||
do {
|
||
let (data, response) = try await session.data(for: request)
|
||
guard let httpResponse = response as? HTTPURLResponse else {
|
||
throw BricksRPCError.invalidResponse
|
||
}
|
||
|
||
let responseHeaders = Dictionary(
|
||
uniqueKeysWithValues: httpResponse.allHeaderFields.compactMap { key, value in
|
||
guard let keyStr = key as? String, let valStr = value as? String else { return nil }
|
||
return (keyStr, valStr)
|
||
}
|
||
)
|
||
|
||
return Response(
|
||
data: data,
|
||
statusCode: httpResponse.statusCode,
|
||
headers: responseHeaders
|
||
)
|
||
} catch {
|
||
throw BricksRPCError.networkError(error)
|
||
}
|
||
}
|
||
|
||
// MARK: - URL解析
|
||
|
||
private func resolveURL(_ url: String) -> String {
|
||
if url.hasPrefix("http://") || url.hasPrefix("https://") {
|
||
return url
|
||
}
|
||
if baseURL.isEmpty {
|
||
return url
|
||
}
|
||
let base = baseURL.hasSuffix("/") ? String(baseURL.dropLast()) : baseURL
|
||
let path = url.hasPrefix("/") ? url : "/\(url)"
|
||
return base + path
|
||
}
|
||
}
|
||
|
||
// MARK: - 错误类型
|
||
|
||
public enum BricksRPCError: Error, LocalizedError {
|
||
case invalidURL(String)
|
||
case invalidResponse
|
||
case networkError(Error)
|
||
case httpError(Int, String)
|
||
|
||
public var errorDescription: String? {
|
||
switch self {
|
||
case .invalidURL(let url): return "无效URL: \(url)"
|
||
case .invalidResponse: return "无效响应"
|
||
case .networkError(let error): return "网络错误: \(error.localizedDescription)"
|
||
case .httpError(let code, let msg): return "HTTP \(code): \(msg)"
|
||
}
|
||
}
|
||
}
|