add swift api client with model types, json decoding
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
82
clients/macos/MagnumOpus/Services/APIClient.swift
Normal file
82
clients/macos/MagnumOpus/Services/APIClient.swift
Normal 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
|
||||
}
|
||||
}
|
||||
55
clients/macos/MagnumOpusTests/APIClientTests.swift
Normal file
55
clients/macos/MagnumOpusTests/APIClientTests.swift
Normal 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.")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user