Skip to content

brightdigit/Spinetail

Repository files navigation

Spinetail Logo

Spinetail

Swift Versions Platforms Documentation License GitHub Workflow Status Maintainability Codecov CodeFactor Grade

A lightweight async Swift client for the Mailchimp Marketing API, built on swift-openapi-generator.

Table of Contents

🎬 Introduction

Spinetail is a narrow, purpose-built async client for the Mailchimp Marketing API. It is intentionally small: it wraps two generated operations β€” listing sent campaigns and fetching a campaign's content β€” and exposes a thin public surface for its consumer (ContributeMailchimp, a newsletter importer): listing sent campaigns, fetching archive HTML, plus convenience helpers for the full content payload and plain text.

Under the hood it wraps a client generated by swift-openapi-generator from Mailchimp's OpenAPI spec, filtered down to just those two operations. The hand-written Spinetail layer adds datacenter-host derivation, HTTP Basic authentication, and a flat campaign DTO.

Looking for the old SwagGen/Prch API? Earlier versions of Spinetail were built on Prch/SwagGen and exposed Mailchimp.API, Client(api:session:), and request/response objects. That architecture was replaced by the swift-openapi-generator migration. The current public surface is MailchimpClient, documented below.

What's a Spinetail?

A Spinetail is a type of Swift bird which shares its habitat with chimps (such as the chimp in Mailchimp).

🎁 Features

  • βœ… Async/await API β€” no completion handlers, no Combine.
  • βœ… Datacenter-aware β€” the API host is derived automatically from your API key's -<datacenter> suffix (e.g. ...-us21 β†’ https://us21.api.mailchimp.com/3.0).
  • βœ… HTTP Basic auth built in β€” pass your API key, nothing else.
  • βœ… Testable by design β€” inject any ClientTransport to replay fixtures instead of hitting the network.
  • βœ… Broad platform support β€” Linux, macOS, iOS, tvOS, watchOS, Windows, Android, and WebAssembly (Wasm).

πŸ— Installation

Add Spinetail as a dependency in your Package.swift:

dependencies: [
  .package(url: "https://github.com/brightdigit/Spinetail.git", from: "1.0.0-alpha.1")
]

Then add it to your target:

.target(
  name: "MyTarget",
  dependencies: [
    .product(name: "Spinetail", package: "Spinetail")
  ]
)

Spinetail requires Swift 6.4 and supports macOS 13+, iOS 16+, tvOS 16+, and watchOS 9+ (plus Linux, Windows, Android, and Wasm).

πŸ’ͺ Usage

Creating a client

Your Mailchimp API key ends in a -<datacenter> suffix; Spinetail uses it to derive the correct API host, so you only need to supply the key:

import Spinetail

let client = try MailchimpClient(
  apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-us21"
)

This convenience initializer uses the default URLSessionTransport. On platforms without URLSession (Wasm), or when you want to inject a custom transport, pass one explicitly:

let client = try MailchimpClient(
  apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-us21",
  transport: myTransport
)

Listing sent campaigns

sentCampaigns(forListID:) returns the sent campaigns for a given Mailchimp list (audience), most recent first, mapped into the flat Campaign model:

let campaigns = try await client.sentCampaigns(forListID: "your-list-id")

for campaign in campaigns {
  print(campaign.title ?? "Untitled", campaign.sendTime ?? .distantPast)
}

Each Campaign exposes the fields the importer reads β€” all optional by design (the consumer validates presence):

Property Source
id id
longArchiveURL long_archive_url
sendTime send_time
subjectLine settings.subject_line
title settings.title
previewText settings.preview_text
segmentText recipients.segment_text
socialCardImageURL social_card.image_url

Fetching a campaign's archive HTML

archiveHTML(forCampaignID:) returns the rendered archive HTML for a campaign:

let html = try await client.archiveHTML(forCampaignID: "campaign-id")

campaignContent(forCampaignID:) and plainText(forCampaignID:) are convenience methods over the same generated getCampaignsIdContent operation β€” campaignContent returns both archive_html and plain_text in one request, and plainText unwraps just the plain-text body.

let content = try await client.campaignContent(forCampaignID: "campaign-id")
let plain = try await client.plainText(forCampaignID: "campaign-id")

Error handling

Invalid HTTP responses and missing content fields are mapped to MailchimpClient.ClientError. Errors thrown by the generated client or transport (for example network failures) propagate unchanged.

Case When
.invalidAPIKey The API key has no -<datacenter> suffix to derive a host.
.invalidResponse The server returned a non-200 / undocumented response.
.missingHTML(campaignID:) The campaign returned no archive_html.
.missingPlainText(campaignID:) The campaign returned no plain_text.
do {
  let html = try await client.archiveHTML(forCampaignID: id)
} catch MailchimpClient.ClientError.missingHTML(let campaignID) {
  print("No archive HTML for \(campaignID)")
}

πŸ› Architecture

Spinetail is two layered targets:

  • SpinetailOpenAPI β€” generated, committed code (Types.swift, Client.swift). Produced ahead of time (not by a build plugin) by Scripts/generate-openapi-spinetail.sh, which filters the full Mailchimp spec down to just getCampaigns and getCampaignsIdContent. Never edit this by hand β€” change the spec/config and regenerate.
  • Spinetail β€” the hand-written public layer and the only target consumers import:
    • MailchimpClient β€” the public client (host derivation + the two operations).
    • AuthenticationMiddleware β€” adds HTTP Basic auth (API key as the password; username ignored by Mailchimp).
    • Campaign β€” the flat DTO mapped from the generated campaign payload.

OpenAPIURLSession (the URLSession transport) does not build for WebAssembly, so anything depending on it is gated behind #if !os(WASI); Wasm callers supply an explicit transport.

πŸ“ž Supported Requests

Spinetail deliberately exposes only what its consumer needs:

Operation Mailchimp endpoint MailchimpClient method
getCampaigns GET /campaigns sentCampaigns(forListID:)
getCampaignsIdContent GET /campaigns/{id}/content archiveHTML(forCampaignID:)

To support more, see Adding an API operation.

πŸ‘· Development

Tooling is pinned in .mise.toml and run through mise exec --. Install once with mise install.

  • Build: swift build
  • Test: swift test (the suite uses swift-testing, not XCTest)
  • Lint + format + build check: ./Scripts/lint.sh
  • Regenerate the OpenAPI client: ./Scripts/generate-openapi-spinetail.sh

Adding an API operation

  1. Add the operation id to filter: operations: in Sources/SpinetailOpenAPI/openapi-generator-config.yaml.
  2. Regenerate with ./Scripts/generate-openapi-spinetail.sh (regeneration must be diff-free unless the spec, config, or generator version changed).
  3. Surface the new operation through MailchimpClient.

πŸ™ Acknowledgments

Thanks to Apple's swift-openapi-generator team, and to Yonas Kolb whose SwagGen powered earlier versions of this package.

πŸ“œ License

This code is distributed under the MIT license. See the LICENSE file for more info.

About

Swift package for interfacing with your Mailchimp account, audiences, campaigns, and more.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages