{"id":19024163,"url":"https://github.com/raykitajima/swiftapiadapter","last_synced_at":"2026-05-15T08:02:46.253Z","repository":{"id":239185060,"uuid":"798808395","full_name":"RayKitajima/SwiftApiAdapter","owner":"RayKitajima","description":"SwiftApiAdapter: Streamline API calls for generative AI, retrieving text and images efficiently in Swift applications.","archived":false,"fork":false,"pushed_at":"2025-01-29T12:47:51.000Z","size":43,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-29T13:37:35.484Z","etag":null,"topics":["api-client","json","jsonapi"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/RayKitajima.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-05-10T14:12:26.000Z","updated_at":"2025-01-29T12:46:38.000Z","dependencies_parsed_at":"2024-09-08T11:08:00.418Z","dependency_job_id":"ae0d1978-260e-4a18-9f0a-bde0631623dc","html_url":"https://github.com/RayKitajima/SwiftApiAdapter","commit_stats":null,"previous_names":["raykitajima/swiftapiadapter"],"tags_count":17,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RayKitajima%2FSwiftApiAdapter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RayKitajima%2FSwiftApiAdapter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RayKitajima%2FSwiftApiAdapter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RayKitajima%2FSwiftApiAdapter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/RayKitajima","download_url":"https://codeload.github.com/RayKitajima/SwiftApiAdapter/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240072063,"owners_count":19743527,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api-client","json","jsonapi"],"created_at":"2024-11-08T20:35:23.013Z","updated_at":"2026-05-15T08:02:46.244Z","avatar_url":"https://github.com/RayKitajima.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# SwiftApiAdapter\n\nSwiftApiAdapter is a Swift Package that streamlines retrieving remote content (JSON, text, images, and web pages) from Swift apps. It’s especially handy for calling generative AI APIs and for loading + extracting web page content.\n\nThis version targets **Swift 6** and uses **structured concurrency** throughout (actors + `async`/`await`) to provide a thread-safe, serial request pipeline.\n\n## Demo App\n\nFor a macOS SwiftUI demonstration project built on this library, see\n[SwiftApiLab](https://github.com/RayKitajima/SwiftApiLab).\n\n## Features\n\n- **Actor-based connector manager** (`ApiConnectorManager`) for safe, concurrent access to per-tag connectors.\n- **Serial execution / rate limiting** via `ApiSerialExecutor` (an actor with a single worker task).\n- **Async/await networking** using `URLSession`.\n- **Immediate vs queued requests**: bypass the serial queue when needed.\n- **Progress / metrics reporting** via `AsyncStream` (no polling required).\n- **Flexible headers** per request, including custom `User-Agent`.\n- **Web page extraction** (content + OpenGraph image) via `ApiContentLoader`.\n\n### Important note about `GET` requests\n\nIn compliance with standard HTTP usage, **SwiftApiAdapter does not attach a request body if the HTTP method is `GET`**.\n\nIf you call an endpoint with `GET` and provide a non-empty body, the body is ignored. If you need to send a payload, use `POST`, `PUT`, etc.\n\n## Installation\n\n### Swift Package Manager\n\nAdd SwiftApiAdapter to your project via Swift Package Manager:\n\n```swift\ndependencies: [\n    .package(url: \"https://github.com/RayKitajima/SwiftApiAdapter.git\", from: \"1.0.0\")\n]\n```\n\n## Usage\n\n### Importing\n\n```swift\nimport SwiftApiAdapter\n```\n\n## Making JSON API requests\n\nUse `ApiRequester.processJsonApi(...)` to call a JSON endpoint.\n\n```swift\nimport SwiftApiAdapter\n\nlet requester = await ApiConnectorManager.shared.getRequester(for: \"ExampleAPI\")\n\nlet endpoint = URL(string: \"https://example.com/api\")!\nlet headers = [\n    \"User-Agent\": \"Your Custom User-Agent\",\n    \"Content-Type\": \"application/json\"\n]\n\nlet response = await requester.processJsonApi(\n    endpoint: endpoint,\n    method: \"POST\",\n    headers: headers,\n    body: #\"{\"hello\":\"world\"}\"#,\n    immediate: false // false = serialized queue, true = bypass queue\n)\n\nprint(response?.responseString ?? \"\u003cno response\u003e\")\n```\n\n### Immediate vs queued execution\n\n- `immediate: false` (default): request is enqueued, executed **serially**, and rate-limited.\n- `immediate: true`: request bypasses the serial queue and executes immediately.\n\n## Loading API content via `ApiContentLoader`\n\n`ApiContentLoader` provides a higher-level interface for calling APIs and extracting values out of the JSON response using a path.\n\n### Example: load a value from JSON\n\n```swift\nlet apiContent = ApiContent(\n    id: UUID(),\n    name: \"Example API Content\",\n    endpoint: \"https://exampleapi.com/data\",\n    method: .get,\n    headers: [\"Authorization\": \"Bearer your_access_token\"],\n    body: \"\",\n    arguments: [\n        // Extract: response[\"data\"][\"result\"]\n        \"result\": \"[\\\"data\\\"][\\\"result\\\"]\"\n    ],\n    extraData: [\"info\": \"additional info\"]\n)\n\ndo {\n    let rack = try await ApiContentLoader.load(\n        contextId: UUID(),\n        apiContent: apiContent\n    )\n\n    if let rack {\n        print(\"Result:\", rack.arguments[\"result\"] ?? \"\u003cmissing\u003e\")\n    } else {\n        print(\"Failed to load API data\")\n    }\n} catch {\n    print(\"Load failed:\", error)\n}\n```\n\n## Loading web page content\n\nYou can also load and extract web page content using the same interface.\n\n```swift\nlet page = ApiContent(\n    id: UUID(),\n    name: \"Web Page Content\",\n    endpoint: \"https://example.com/page\",\n    method: .get,\n    headers: [:],\n    body: \"\",\n    contentType: .page\n)\n\ndo {\n    let rack = try await ApiContentLoader.load(\n        contextId: UUID(),\n        apiContent: page\n    )\n\n    if let rack {\n        print(\"content:\", rack.arguments[\"content\"] ?? \"\u003cmissing\u003e\")\n        print(\"url:\", rack.arguments[\"url\"] ?? \"\u003cmissing\u003e\")\n        print(\"ogimage:\", rack.arguments[\"ogimage\"] ?? \"\u003cmissing\u003e\")\n        print(\"finalUrl:\", rack.arguments[\"finalUrl\"] ?? \"\u003cmissing\u003e\")\n    }\n} catch {\n    print(\"Load failed:\", error)\n}\n```\n\n## Observing request metrics (structured concurrency)\n\n`ApiSerialExecutor` reports progress via `AsyncStream`.\n\n```swift\nlet executor = await ApiConnectorManager.shared.getExecutor(for: \"ExampleAPI\")\n\nTask {\n    for await metrics in await executor.metricsUpdates() {\n        print(\"Executed \\(metrics.cumulativeExecuted) / \\(metrics.cumulativeRequested)\")\n    }\n}\n```\n\nThis works well for logging, CLI tools, or bridging into UI state.\n\n## SwiftUI integration\n\nBelow is one simple way to bridge `AsyncStream` metrics into SwiftUI using an `ObservableObject`.\n\n```swift\nimport SwiftUI\nimport SwiftApiAdapter\n\n@MainActor\nfinal class ApiController: ObservableObject {\n    @Published var cumulativeRequested: Int = 0\n    @Published var cumulativeExecuted: Int = 0\n\n    private var metricsTask: Task\u003cVoid, Never\u003e?\n\n    func observeMetrics(tag: String) {\n        metricsTask?.cancel()\n        metricsTask = Task {\n            let executor = await ApiConnectorManager.shared.getExecutor(for: tag)\n            for await metrics in await executor.metricsUpdates() {\n                cumulativeRequested = metrics.cumulativeRequested\n                cumulativeExecuted  = metrics.cumulativeExecuted\n            }\n        }\n    }\n\n    deinit {\n        metricsTask?.cancel()\n    }\n}\n\nstruct ApiView: View {\n    @StateObject var apiController = ApiController()\n\n    var body: some View {\n        HStack(spacing: 6) {\n            Text(\"Generating\")\n            Text(\"(\\(apiController.cumulativeExecuted)/\\(apiController.cumulativeRequested))\")\n                .foregroundStyle(.secondary)\n            Image(systemName: \"ellipsis\")\n        }\n        .task {\n            apiController.observeMetrics(tag: \"ExampleAPI\")\n        }\n    }\n}\n```\n\n## Managing connectors\n\n`ApiConnectorManager` is an **actor**, so calls from outside the actor require `await`.\n\n```swift\n// Get a connector / requester / executor\nlet connector  = await ApiConnectorManager.shared.getConnector(for: \"Tag\")\nlet requester  = await ApiConnectorManager.shared.getRequester(for: \"Tag\")\nlet executor   = await ApiConnectorManager.shared.getExecutor(for: \"Tag\")\n\n// Clear one connector (stops its executor)\nawait ApiConnectorManager.shared.clearConnector(for: \"Tag\")\n\n// Clear all connectors\nawait ApiConnectorManager.shared.clearAllConnectors()\n```\n\n## Contributing\n\nContributions are welcome — feel free to open issues or submit PRs.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fraykitajima%2Fswiftapiadapter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fraykitajima%2Fswiftapiadapter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fraykitajima%2Fswiftapiadapter/lists"}