add swift api client with model types, json decoding

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 12:17:01 +01:00
parent 7bb74654c5
commit 1b24cb721a
2 changed files with 137 additions and 0 deletions

View File

@@ -0,0 +1,82 @@
import Foundation
struct ThreadSummary: Codable, Identifiable {
let threadId: String
let subject: String
let authors: String
let totalMessages: Int
let tags: String
let timestamp: Int
let accountId: String
var id: String { threadId }
var tagList: [String] {
tags.split(separator: ",").map(String.init)
}
var isUnread: Bool {
tagList.contains("unread")
}
var date: Date {
Date(timeIntervalSince1970: TimeInterval(timestamp))
}
}
struct EmailMessage: Codable, Identifiable {
let messageId: String
let threadId: String
let fromHeader: String
let toHeader: String
let subject: String
let date: String
let inReplyTo: String
let body: String
let tags: String
let timestamp: Int
let accountId: String
var id: String { messageId }
var senderName: String {
if let range = fromHeader.range(of: " <") {
return String(fromHeader[..<range.lowerBound])
}
return fromHeader
}
}
struct ThreadListResponse: Codable {
let threads: [ThreadSummary]
}
struct MessageListResponse: Codable {
let messages: [EmailMessage]
}
@Observable
final class APIClient {
private let baseURL: URL
init(baseURL: URL) {
self.baseURL = baseURL
}
func fetchThreads(accountId: String) async throws -> [ThreadSummary] {
var components = URLComponents(
url: baseURL.appendingPathComponent("api/threads"),
resolvingAgainstBaseURL: false
)!
components.queryItems = [URLQueryItem(name: "accountId", value: accountId)]
let (data, _) = try await URLSession.shared.data(from: components.url!)
return try JSONDecoder().decode(ThreadListResponse.self, from: data).threads
}
func fetchMessages(threadId: String) async throws -> [EmailMessage] {
let url = baseURL.appendingPathComponent("api/threads/\(threadId)/messages")
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(MessageListResponse.self, from: data).messages
}
}

View File

@@ -0,0 +1,55 @@
import Testing
@testable import MagnumOpus
@Suite("APIClient")
struct APIClientTests {
@Test("decodes thread list from JSON")
func decodesThreadList() throws {
let json = """
{
"threads": [
{
"threadId": "t001",
"subject": "Q1 Planning",
"authors": "Alice, Bob",
"totalMessages": 3,
"tags": "inbox,unread",
"timestamp": 1709884532,
"accountId": "personal"
}
]
}
""".data(using: .utf8)!
let response = try JSONDecoder().decode(ThreadListResponse.self, from: json)
#expect(response.threads.count == 1)
#expect(response.threads[0].subject == "Q1 Planning")
}
@Test("decodes message list from JSON")
func decodesMessageList() throws {
let json = """
{
"messages": [
{
"messageId": "msg001@example.com",
"threadId": "t001",
"fromHeader": "Alice <alice@example.com>",
"toHeader": "user@example.com",
"subject": "Q1 Planning",
"date": "2024-03-08T10:15:32+01:00",
"inReplyTo": "",
"body": "Hey, let's plan Q1.",
"tags": "inbox,unread",
"timestamp": 1709884532,
"accountId": "personal"
}
]
}
""".data(using: .utf8)!
let response = try JSONDecoder().decode(MessageListResponse.self, from: json)
#expect(response.messages.count == 1)
#expect(response.messages[0].body == "Hey, let's plan Q1.")
}
}