Skip to content
  •  
  •  
  •  
5 changes: 5 additions & 0 deletions .github/workflows/claude.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ jobs:
Use `gh pr comment` to post a summary review comment on the PR.
Use inline comments for specific code issues.
Be concise and actionable.
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

claude-respond:
# Only respond to @claude mentions from adamayoung
Expand All @@ -62,3 +64,6 @@ jobs:
- uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
trigger_phrase: "@claude"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
36 changes: 27 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ swift test --filter "TestClassName"
swift test --filter "TestClassName/testMethodName"

# Code Quality
make lint # Check swift-format compliance
make format # Auto-format code
make lint-markdown # Lint markdown files
make format # Auto-format code (requires swiftlint and swiftformat)
make lint # Check swiftlint and swiftformat compliance
make lint-markdown # Lint markdown files (requires markdownlint)

# Documentation
make preview-docs # Preview DocC locally
Expand Down Expand Up @@ -85,16 +85,32 @@ HTTPClient (protocol)
- `Tests/TMDbIntegrationTests/` - Live API tests
- Uses Swift Testing framework (not XCTest)

### Test Writing Guidelines

**CRITICAL: Never use force unwrapping in tests**
- Always use `#require()` instead of force unwrapping (`!`) when working with optionals
- `#require()` provides better error messages and handles nil gracefully
- Example:
```swift
// ❌ BAD - force unwrap
let translation = result.translations.first { $0.languageCode == "en" }!

// βœ… GOOD - use #require
let translation = try #require(result.translations.first { $0.languageCode == "en" })
```

## Code Style Requirements

Enforced via `.swift-format`:
Enforced via `swiftlint` and `swiftformat`:

- **Line length:** 100 characters
- **All public declarations must have documentation** (`///` style)
- **No force unwrapping** (`!`) or force try (`try!`)
- **Use guard for early exits**
- **No leading underscores** - use file-private instead

**Note:** The `swiftlint` and `swiftformat` tools must be installed separately. If not available, ensure code follows existing style patterns and compiles without warnings.

## Testing Requirements

**CRITICAL: Always run both unit tests AND integration tests after making code changes.**
Expand Down Expand Up @@ -141,13 +157,15 @@ Unit tests alone may pass even when:

**CRITICAL: Before considering ANY task complete, run these steps in order:**

1. **Format code**: `make format` - Auto-format all Swift files
2. **Check lint**: `make lint` - Verify swift-format compliance
3. **Run unit tests**: `make test` - All unit tests must pass
4. **Run integration tests**: `make integration-test` - All integration tests must pass
1. **Format code** (if tools available): `make format` - Auto-format all Swift files with swiftlint and swiftformat
- If tools not installed: Manually verify code follows existing style patterns
2. **Check lint** (if tools available): `make lint` - Verify swiftlint and swiftformat compliance
- If tools not installed: Ensure code compiles with `swift build -Xswiftc -warnings-as-errors`
3. **Run unit tests**: `make test` - All unit tests must pass βœ… **REQUIRED**
4. **Run integration tests**: `make integration-test` - All integration tests must pass βœ… **REQUIRED**
5. **Build documentation**: `make build-docs` - Verify DocC builds without warnings (if public API changed)

**All steps must succeed before the work is complete.**
**Required steps (3-5) must succeed before the work is complete. Steps 1-2 are strongly recommended but can be skipped if formatting tools are not available.**

## Documentation Requirements

Expand Down
3 changes: 1 addition & 2 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
// swift-tools-version:6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

import class Foundation.ProcessInfo
import PackageDescription

let package = Package(
name: "TMDb",
Expand Down
6 changes: 3 additions & 3 deletions Sources/TMDb/Adapters/URLSessionHTTPClientAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ final class URLSessionHTTPClientAdapter: HTTPClient {
throw error
}

let httpResponse = Self.httpResponse(from: data, response: response)
return httpResponse
return Self.httpResponse(from: data, response: response)
}

}
Expand Down Expand Up @@ -73,7 +72,8 @@ extension URLSessionHTTPClientAdapter {

guard let data, let response else {
continuation.resume(
throwing: NSError(domain: "uk.co.adam-young.TMDb", code: -1))
throwing: NSError(domain: "uk.co.adam-young.TMDb", code: -1)
)
return
}

Expand Down
4 changes: 3 additions & 1 deletion Sources/TMDb/Domain/Models/Certification.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ public struct Certification: Identifiable, Codable, Equatable, Hashable, Sendabl
///
/// Certification's identifier (same as ``code``).
///
public var id: String { code }
public var id: String {
code
}

///
/// Certification code.
Expand Down
10 changes: 6 additions & 4 deletions Sources/TMDb/Domain/Models/CollectionTranslation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ public struct CollectionTranslation: Identifiable, Codable, Equatable, Hashable,
///
/// Collection translation's identifier (same as `languageCode`).
///
public var id: String { languageCode }
public var id: String {
languageCode
}

///
/// ISO 3166-1 country code.
Expand Down Expand Up @@ -71,10 +73,10 @@ public struct CollectionTranslation: Identifiable, Codable, Equatable, Hashable,
extension CollectionTranslation {

private enum CodingKeys: String, CodingKey {
case countryCode = "iso_3166_1"
case languageCode = "iso_639_1"
case countryCode = "iso31661"
case languageCode = "iso6391"
case name
case englishName = "english_name"
case englishName
case data
}

Expand Down
4 changes: 3 additions & 1 deletion Sources/TMDb/Domain/Models/Country.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ public struct Country: Identifiable, Codable, Equatable, Hashable, Sendable {
///
/// Country's identifier (same as `countryCode`).
///
public var id: String { countryCode }
public var id: String {
countryCode
}

///
/// The ISO 3166-1 country code.
Expand Down
4 changes: 3 additions & 1 deletion Sources/TMDb/Domain/Models/Department.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ public struct Department: Identifiable, Codable, Equatable, Hashable, Sendable {
///
/// Departments's identifier (same as `name`).
///
public var id: String { name }
public var id: String {
name
}

///
/// Department's name.
Expand Down
4 changes: 3 additions & 1 deletion Sources/TMDb/Domain/Models/ImageMetadata.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ public struct ImageMetadata: Identifiable, Codable, Equatable, Hashable, Sendabl
///
/// Image metadata's identifier (same as `filePath`).
///
public var id: URL { filePath }
public var id: URL {
filePath
}

///
/// Path of the image.
Expand Down
4 changes: 3 additions & 1 deletion Sources/TMDb/Domain/Models/Language.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ public struct Language: Identifiable, Codable, Equatable, Hashable, Sendable {
///
/// Language code.
///
public var id: String { code }
public var id: String {
code
}

///
/// The ISO 639-1 language code.
Expand Down
4 changes: 3 additions & 1 deletion Sources/TMDb/Domain/Models/ProductionCountry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ public struct ProductionCountry: Identifiable, Codable, Equatable, Hashable, Sen
///
/// Country's identifier (same as `countryCode`).
///
public var id: String { countryCode }
public var id: String {
countryCode
}

///
/// The ISO 3166-1 country code.
Expand Down
4 changes: 3 additions & 1 deletion Sources/TMDb/Domain/Models/SpokenLanguage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ public struct SpokenLanguage: Identifiable, Codable, Equatable, Hashable, Sendab
///
/// Language code.
///
public var id: String { languageCode }
public var id: String {
languageCode
}

///
/// The ISO 639-1 language code.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,9 @@ final class TMDbAuthenticationService: AuthenticationService {
}

func authenticateURL(for token: Token, redirectURL: URL? = nil) -> URL {
let url = authenticateURLBuilder.authenticateURL(
authenticateURLBuilder.authenticateURL(
with: token.requestToken, redirectURL: redirectURL
)

return url
}

func createSession(withToken token: Token) async throws -> Session {
Expand Down Expand Up @@ -84,9 +82,7 @@ final class TMDbAuthenticationService: AuthenticationService {
throw TMDbError(error: error)
}

let session = try await createSession(withToken: validatedToken)

return session
return try await createSession(withToken: validatedToken)
}

@discardableResult
Expand Down
3 changes: 2 additions & 1 deletion Sources/TMDb/Networking/TMDbAPIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ extension TMDbAPIClient {
var queryItems = urlComponents.queryItems ?? []
for requestQueryItem in requestQueryItems {
queryItems.append(
URLQueryItem(name: requestQueryItem.key, value: requestQueryItem.value))
URLQueryItem(name: requestQueryItem.key, value: requestQueryItem.value)
)
}

urlComponents.queryItems = queryItems
Expand Down
4 changes: 4 additions & 0 deletions Sources/TMDb/TMDb.docc/TMDb.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ Watch providers provided by [JustWatch](https://www.justwatch.com).

- ``TVSeriesService``
- ``TVSeries``
- ``TVSeriesPageableList``
- ``ShowCredits``
- ``TVSeriesAggregateCredits``
- ``AggregateCastMember``
Expand All @@ -85,6 +86,7 @@ Watch providers provided by [JustWatch](https://www.justwatch.com).
- ``TVSeriesExternalLinksCollection``
- ``TVSeriesImageFilter``
- ``TVSeriesVideoFilter``
- ``ContentRating``

### TV Seasons

Expand Down Expand Up @@ -122,9 +124,11 @@ Watch providers provided by [JustWatch](https://www.justwatch.com).
- ``MoviePageableList``
- ``Movie``
- ``MovieSort``
- ``DiscoverMovieFilter``
- ``TVSeriesPageableList``
- ``TVSeries``
- ``TVSeriesSort``
- ``DiscoverTVSeriesFilter``

### Trending

Expand Down
1 change: 0 additions & 1 deletion Tests/TMDbIntegrationTests/AccountIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import Foundation
import Testing

@testable import TMDb

@Suite(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import Foundation
import Testing

@testable import TMDb

@Suite(
Expand Down Expand Up @@ -67,4 +66,24 @@ struct AuthenticationIntegrationTests {
#expect(isValid)
}

@Test("authenticateURL returns valid URL")
func authenticateURLReturnsValidURL() async throws {
let token = try await authenticationService.requestToken()

let url = authenticationService.authenticateURL(for: token, redirectURL: nil)

#expect(url.host == "www.themoviedb.org")
#expect(url.path.contains("authenticate") == true)
}

@Test("authenticateURL with redirect URL returns valid URL")
func authenticateURLWithRedirectReturnsValidURL() async throws {
let token = try await authenticationService.requestToken()
let redirectURL = try #require(URL(string: "myapp://callback"))

let url = authenticationService.authenticateURL(for: token, redirectURL: redirectURL)

#expect(url.absoluteString.contains("redirect_to") == true)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import Foundation
import Testing

@testable import TMDb

@Suite(
Expand Down
20 changes: 7 additions & 13 deletions Tests/TMDbIntegrationTests/CollectionIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import Foundation
import Testing

@testable import TMDb

@Suite(
Expand Down Expand Up @@ -79,21 +78,16 @@ struct CollectionIntegrationTests {
#expect(!imageCollection.posters.isEmpty)
}

@Test("translations", .disabled("Known issue - needs investigation"))
@Test("translations")
func translations() async throws {
let collectionID = 10

do {
let translations = try await collectionService.translations(
forCollection: collectionID
)

#expect(!translations.isEmpty)
#expect(translations.contains(where: { $0.languageCode == "en" }))
} catch {
Issue.record("Failed with error: \(error)")
throw error
}
let translations = try await collectionService.translations(
forCollection: collectionID
)

#expect(!translations.isEmpty)
#expect(translations.contains(where: { $0.languageCode == "en" }))
}

}
1 change: 0 additions & 1 deletion Tests/TMDbIntegrationTests/CompanyIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import Foundation
import Testing

@testable import TMDb

@Suite(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import Foundation
import Testing

@testable import TMDb

@Suite(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import Foundation
import Testing

@testable import TMDb

@Suite(
Expand Down
1 change: 0 additions & 1 deletion Tests/TMDbIntegrationTests/FindIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import Foundation
import Testing

@testable import TMDb

@Suite(
Expand Down
Loading
Loading