import Foundation struct FeishuAPIResponse: Decodable { let code: Int let msg: String let data: T? enum CodingKeys: String, CodingKey { case code, msg, message, data } init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) code = try c.decode(Int.self, forKey: .code) // Some endpoints use "msg", others use "message" if let m = try c.decodeIfPresent(String.self, forKey: .msg) { msg = m } else { msg = try c.decode(String.self, forKey: .message) } data = try c.decodeIfPresent(T.self, forKey: .data) } } struct RecordListData: Decodable { let items: [RecordItem] let hasMore: Bool let pageToken: String? let total: Int? } struct RecordItem: Decodable { let recordId: String let fields: [String: JSONValue] } struct OAuthTokenData: Decodable { // Note: decoder uses .convertFromSnakeCase, so JSON keys are auto-mapped: // access_token→accessToken, token_type→tokenType, expires_in→expiresIn, etc. let accessToken: String let tokenType: String? let expiresIn: Int let refreshToken: String? let refreshExpiresIn: Int? } struct TenantAccessTokenData: Decodable { let code: Int let msg: String let tenantAccessToken: String let expire: Int } struct UserInfoData: Decodable { let name: String? // en_name → enName via .convertFromSnakeCase let enName: String? } enum JSONValue: Decodable { case string(String) case number(Double) case bool(Bool) case array([JSONValue]) case object([String: JSONValue]) case null init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() if container.decodeNil() { self = .null } else if let value = try? container.decode(Bool.self) { self = .bool(value) } else if let value = try? container.decode(Double.self) { self = .number(value) } else if let value = try? container.decode(String.self) { self = .string(value) } else if let value = try? container.decode([JSONValue].self) { self = .array(value) } else if let value = try? container.decode([String: JSONValue].self) { self = .object(value) } else { throw DecodingError.dataCorruptedError( in: container, debugDescription: "Unsupported JSON value" ) } } var stringValue: String? { if case let .string(value) = self { return value } if case let .number(value) = self { return String(value) } return nil } var firstUserName: String? { guard case let .array(items) = self else { return nil } for item in items { if case let .object(fields) = item, let name = fields["name"]?.stringValue { return name } } return nil } } enum BugMapper { static func map( _ record: RecordItem, config: AppConfig ) -> Bug { let fields = record.fields let mappings = config.fieldMappings return Bug( id: record.recordId, title: stringValue(in: fields, key: mappings.titleField) ?? "Untitled", priority: BugPriority(rawValue: stringValue(in: fields, key: mappings.priorityField) ?? "") ?? .unknown, status: mapStatus(stringValue(in: fields, key: mappings.statusField), mappings: mappings), assignee: userName(in: fields, key: mappings.assigneeField) ?? "Unknown", reporter: userName(in: fields, key: mappings.reporterField), createdAt: FeishuDateParser.parse(rawValue(in: fields, key: mappings.createdAtField)) ?? .distantPast, updatedAt: FeishuDateParser.parse(rawValue(in: fields, key: mappings.updatedAtField)) ?? .distantPast, feishuURL: FeishuURLBuilder.recordURL( baseDomain: config.feishuBaseDomain, appToken: config.appToken, tableId: config.tableId, recordId: record.recordId ) ) } private static func rawValue(in fields: [String: JSONValue], key: String) -> Any? { guard let value = fields[key] else { return nil } switch value { case let .string(string): return string case let .number(number): return number case let .bool(bool): return bool case let .array(array): return array.compactMap { item -> String? in if case let .object(fields) = item { return fields["name"]?.stringValue } return item.stringValue } case let .object(object): return object["name"]?.stringValue ?? object["text"]?.stringValue case .null: return nil } } private static func stringValue(in fields: [String: JSONValue], key: String) -> String? { fields[key]?.stringValue } private static func userName(in fields: [String: JSONValue], key: String) -> String? { fields[key]?.firstUserName ?? fields[key]?.stringValue } private static func mapStatus(_ feishuValue: String?, mappings: AppConfig.FieldMappings) -> BugStatus { guard let value = feishuValue else { return .unknown } // Look up in the user-configured mapping; fall back to .unknown (shown as active) return mappings.statusMappings[value] ?? .unknown } }