import Foundation struct FeishuAPIResponse: Decodable { let code: Int let msg: String let data: T? } 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: BugStatus(rawValue: stringValue(in: fields, key: mappings.statusField) ?? "") ?? .unknown, 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 } }