// ----------------------------------------------------------------------------- // Lou-Ka — Agrégateur de logements à louer (province de Québec) // Auteur : Simon-Pierre Boucher — contact@spboucher.ai // API.swift : client HTTP vers l'API de production (www.lou-ka.com) // ----------------------------------------------------------------------------- import Foundation struct ListingFilters: Equatable { var q = "" var city = "" var sector = "" var unitType = "" var source = "" var priceMin: Int? var priceMax: Int? /// nil = peu importe · 0 = maintenant · 30/60/90 = d'ici n jours var dispoDays: Int? var petsOk = false /// nil = peu importe · true = meublé · false = non meublé var furnished: Bool? var areaMin: Int? var activeCount: Int { [q, city, sector, unitType, source].filter { !$0.isEmpty }.count + (priceMin != nil ? 1 : 0) + (priceMax != nil ? 1 : 0) + (dispoDays != nil ? 1 : 0) + (petsOk ? 1 : 0) + (furnished != nil ? 1 : 0) + (areaMin != nil ? 1 : 0) } } enum APIError: LocalizedError { case badStatus(Int) var errorDescription: String? { switch self { case .badStatus(let code): return "L'API a répondu \(code). Réessayez dans un instant." } } } enum API { static let base = URL(string: "https://www.lou-ka.com")! /// Date ISO à +n jours (paramètre `available_by`) static func isoInDays(_ n: Int) -> String { let d = Calendar.current.date(byAdding: .day, value: n, to: Date()) ?? Date() let f = DateFormatter() f.dateFormat = "yyyy-MM-dd" f.locale = Locale(identifier: "en_US_POSIX") return f.string(from: d) } static func get(_ path: String, query: [URLQueryItem] = []) async throws -> T { var comps = URLComponents(url: base.appendingPathComponent(path), resolvingAgainstBaseURL: false)! if !query.isEmpty { comps.queryItems = query } var req = URLRequest(url: comps.url!) req.timeoutInterval = 20 req.setValue("LouKa-iOS/1.2", forHTTPHeaderField: "User-Agent") let (data, resp) = try await URLSession.shared.data(for: req) guard let http = resp as? HTTPURLResponse, (200..<300).contains(http.statusCode) else { throw APIError.badStatus((resp as? HTTPURLResponse)?.statusCode ?? -1) } return try JSONDecoder().decode(T.self, from: data) } static func listings(_ f: ListingFilters, limit: Int? = nil) async throws -> ListingsResponse { var q: [URLQueryItem] = [] if !f.q.isEmpty { q.append(.init(name: "q", value: f.q)) } if !f.city.isEmpty { q.append(.init(name: "city", value: f.city)) } if !f.sector.isEmpty { q.append(.init(name: "sector", value: f.sector)) } if !f.unitType.isEmpty { q.append(.init(name: "unit_type", value: f.unitType)) } if !f.source.isEmpty { q.append(.init(name: "source", value: f.source)) } if let min = f.priceMin { q.append(.init(name: "price_min", value: String(min))) } if let max = f.priceMax { q.append(.init(name: "price_max", value: String(max))) } if f.petsOk { q.append(.init(name: "pets", value: "oui")) } if let furn = f.furnished { q.append(.init(name: "furnished", value: furn ? "1" : "0")) } if let days = f.dispoDays { q.append(.init(name: "available_by", value: isoInDays(days))) } if let area = f.areaMin { q.append(.init(name: "area_min", value: String(area))) } if let limit { q.append(.init(name: "limit", value: String(limit))) } return try await get("api/listings", query: q) } static func listing(uid: String) async throws -> Listing { try await get("api/listings/\(uid)") } static func facets(city: String? = nil) async throws -> Facets { var q: [URLQueryItem] = [] if let city, !city.isEmpty { q.append(.init(name: "city", value: city)) } return try await get("api/facets", query: q) } static func sources() async throws -> SourcesResponse { try await get("api/sources") } static func stats() async throws -> Stats { try await get("api/stats") } static func detailedStats() async throws -> DetailedStats { try await get("api/stats/detailed") } }