Contributing to SwiftSonic
July 20, 2026 · View on GitHub
Thanks for taking the time to contribute! This document covers everything you need to get started.
Table of contents
- Code of conduct
- Reporting bugs
- Suggesting features
- Development setup
- Making changes
- Coding conventions
- Adding an endpoint
- Pull requests
Code of conduct
Be kind and constructive. Harassment, dismissiveness, or personal attacks have no place here.
Reporting bugs
Open a GitHub issue with:
- A short, descriptive title
- The SwiftSonic version (
0.1.0,main, etc.) - A minimal reproduction — ideally a failing test or a short code snippet
- The actual vs. expected behaviour
Suggesting features
Open a GitHub issue with the enhancement label. Describe the use case first, then the proposed API. If you have a concrete implementation in mind, feel free to sketch it out.
Development setup
git clone https://github.com/CassetteLab/swiftsonic.git
cd swiftsonic
swift build
swift test
No external tools are required. The project has zero dependencies.
Recommended environment
- Xcode 16+ or any editor with Swift LSP support (VS Code + SourceKit-LSP)
- Swift 6.0 toolchain (ships with Xcode 16)
Making changes
- Fork the repository and create a feature branch from
main:git checkout -b feat/my-feature - Write your code and tests (see Adding an endpoint for the standard pattern).
- Run the full test suite locally:
swift test - Commit with a conventional commit message (see below) and open a PR.
Commit message format
<type>: <short description>
[optional body]
Types: feat, fix, refactor, test, docs, ci, chore.
Examples:
feat: add getSimilarSongs2 endpoint
fix: correct date decoding for starred field
docs: add usage example for search3
Coding conventions
- Swift 6 strict concurrency — all public types must conform to
Sendable, zero actor-isolation warnings - No force unwraps (
!) in library code - No external dependencies — only
FoundationandCryptoKit - Formatting — 4-space indentation, no trailing whitespace
- Access control —
publicfor anything consumers need,internal/privatefor everything else - Tests — every new endpoint needs at least a decode test and a params test (see below)
Adding an endpoint
Follow the pattern used throughout the codebase:
1. Add the model (if new types are needed)
Create Sources/SwiftSonic/Models/MyModel.swift. Conform to Decodable, Sendable, and Identifiable (if the type has an id).
public struct MyModel: Decodable, Sendable, Identifiable {
public let id: String
public let name: String
}
2. Add the payload type and endpoint method
In the relevant SwiftSonicClient+Domain.swift file (or create a new one):
private struct MyPayload: SubsonicPayload {
static let payloadKey = "myResult" // the JSON key inside "subsonic-response"
let myResult: MyModel
init(from decoder: any Decoder) throws {
myResult = try decoder.singleValueContainer().decode(MyModel.self)
}
}
public extension SwiftSonicClient {
func myEndpoint(id: String) async throws -> MyModel {
let envelope: SubsonicEnvelope<MyPayload> =
try await performDecode(endpoint: "myEndpoint", params: ["id": id])
guard let result = envelope.payload?.myResult else {
throw SwiftSonicError.decoding(
DecodingError.valueNotFound(MyModel.self,
DecodingError.Context(codingPath: [], debugDescription: "Missing payload")),
rawData: Data()
)
}
return result
}
}
3. Add a JSON fixture
Create Tests/SwiftSonicTests/Fixtures/myEndpoint.json with a realistic sample response.
4. Write tests
Create or extend a test file. Every endpoint needs at minimum:
- A decode test — verifies the fixture decodes into the expected model values
- A params test — verifies the correct query parameters are sent
@Suite("myEndpoint")
struct MyEndpointTests {
@Test("myEndpoint decodes result")
func decodesResult() async throws {
let mock = MockHTTPTransport()
mock.enqueue(fixture: "myEndpoint")
let client = SwiftSonicClient(configuration: .test, transport: mock)
let result = try await client.myEndpoint(id: "42")
#expect(result.id == "42")
#expect(result.name == "Expected Name")
}
@Test("myEndpoint sends id param")
func sendsIdParam() async throws {
let mock = MockHTTPTransport()
mock.enqueue(fixture: "myEndpoint")
let client = SwiftSonicClient(configuration: .test, transport: mock)
_ = try await client.myEndpoint(id: "42")
#expect(mock.queryItem(named: "id") == "42")
}
}
5. Update the README
Add the endpoint to the coverage table in README.md.
Pull requests
- Target the
mainbranch - One logical change per PR
- The CI must be green before merging
- Include a short description of what changed and why
For large or breaking changes, open an issue first to discuss the approach before investing time in an implementation.