tixster/swiki
Typed Swift client for the Shikimori API (`v1`, `v2`, `GraphQL`) with OAuth2 support, automatic token refresh, and GraphQL operation generation.
Features
- API versioned clients:
swiki.v1,swiki.v2,swiki.graphQL. - Dedicated subclients per resource (
users,animes,userRates,topicIgnore, etc.). - Unified CRUD interface (
list,get,create,update,delete) plus resource-specific methods. - OAuth2:
- exchange authorization_code for a token, - manual and automatic token refresh, - ASWebAuthenticationSession on Apple platforms.
- Token storage via
SwikiOAuthTokenStore:
- Keychain by default on Apple platforms, - custom storage for other platforms.
- GraphQL:
- raw queries, - typed operations (SwikiGraphQLOperation), - generate operations from .graphql files (one .swift file per operation).
Requirements
- Swift
6.2+ - Platforms:
- iOS 16+ - macOS 13+ - tvOS 16+ - watchOS 9+ - Linux
Installation (Swift Package Manager)
dependencies: [
.package(url: "https://github.com/Tixster/Swiki.git", .upToNextMajor(from: "1.0.0"))
]targets: [
.target(
name: "YourTarget",
dependencies: [
.product(name: "Swiki", package: "Swiki")
]
)
]If you only need the models:
.product(name: "SwikiModels", package: "Swiki")Quick Start
import Swiki
let config = SwikiConfiguration(
userAgent: "MyApp/1.0 (me@example.com)"
)
let swiki = Swiki(configuration: config)
let users = try await swiki.v1.users.list(
query: SwikiV1UsersSearchQuery(
search: "kirito",
limit: 5
)
)Configuration
SwikiConfiguration:
userAgent(required)clientId/accessToken(static authorization)oauthCredentials+oauthTokenStore(OAuth2)oauthBaseURL(default:https://shikimori.io)graphQLURL(default:https://shikimori.io/api/graphql)baseURL(default:https://shikimori.io/api)apiLogger(swift-logLoggerfor API request logging)additionalHeadersisRpsRpmRestrictionsEnabled(trueby default)
API Logging (swift-log)
import Swiki
import Logging
LoggingSystem.bootstrap(StreamLogHandler.standardOutput)
let logger = Logger(label: "com.example.swiki.api")
let config = SwikiConfiguration(
userAgent: "MyApp/1.0 (me@example.com)",
apiLogger: logger
)Log metadata includes: kind, method, url, attempt, status, duration_ms, request/response body size, and error text.
OAuth2
1) Initialization
import Swiki
let credentials = SwikiOAuthCredentials(
clientId: "<client_id>",
clientSecret: "<client_secret>",
redirectURI: "myapp://oauth-callback"
)
let config = SwikiConfiguration(
userAgent: "MyApp/1.0 (me@example.com)",
oauthCredentials: credentials
)
let swiki = Swiki(configuration: config)2) Apple platforms: ASWebAuthenticationSession
#if canImport(AuthenticationServices)
let token = try await swiki.oauth?.authorizeWithWebAuthenticationSession(
scopes: [.userRates, .comments, .topics]
)
#endif3) Universal flow (manual)
guard let oauth = swiki.oauth else { fatalError("OAuth is not configured") }
let url = try oauth.authorizationURL(scopes: [.userRates, .comments])
// Open url in a browser and get `code` from your redirect URI
let token = try await oauth.exchangeCode("<authorization_code>")4) Token refresh
- Automatically:
- on 401, the request is retried after refreshTokenIfPossible(); - when a token expires, validAccessToken() attempts refresh.
- Manually:
let newToken = try await swiki.oauth?.refreshToken()5) Token storage
- Apple platforms:
SwikiKeychainOAuthTokenStoreis used by default. - Other platforms: provide your own
SwikiOAuthTokenStoreimplementation.
public struct CustomTokenStore: SwikiOAuthTokenStore {
public init() {}
public func loadToken() async throws -> SwikiOAuthToken? {
nil
}
public func saveToken(_ token: SwikiOAuthToken?) async throws {
// persist token
}
}REST API
Structure
swiki.v1.<resource>
swiki.v2.<resource>Basic subclient methods
Most subclients expose:
list(query:)for collection endpoints with filters/pagination.get(id:)create(body:)update(id:body:method:)delete(id:)- resource-specific methods (for example
roles(id:),whoami(),increment(id:)). queryparameters are available only on endpoints where Shikimori API supports them.request(...)for arbitrary methods/actions.
Typed Queries (v1/v2)
For endpoints that accept query parameters, v1/v2 clients use concrete typed query models from Sources/Swiki/Queries:
- typed query models (
SwikiV1AnimesQuery,SwikiV1UsersSearchQuery,SwikiV1UsersRatesQuery,SwikiV1TopicsQuery,SwikiV1CommentsQuery,SwikiV2UserRatesQuery, etc.). SwikiQueryis still used only for endpoints with free-form query payloads.
let animes = try await swiki.v1.animes.list(
query: SwikiV1AnimesQuery(
page: 1,
limit: 5,
order: .ranked,
status: .released,
search: "bakemonogatari"
)
)
let rates = try await swiki.v2.userRates.list(
query: SwikiV2UserRatesQuery(
page: 1,
limit: 20,
userId: "123",
targetType: .anime,
status: .watching
)
)V1 resources
achievements, animes, appears, bans, calendars, characters, clubs, comments, constants, dialogs, favorites, forums, friends, genres, ignores, mangas, messages, people, publishers, ranobe, reviews, stats, studios, styles, topicIgnores, topics, userImages, userRates, users, videos.
V2 resources
abuseRequests, episodeNotifications, topicIgnore, userIgnore, userRates.
REST examples
// v1 users
let user = try await swiki.v1.users.user(id: "1")
let whoami = try await swiki.v1.users.whoami()
// v1 animes custom route
let roles = try await swiki.v1.animes.roles(id: "1")
// v2 user rates
let rate = try await swiki.v2.userRates.get(id: "100")
let updated = try await swiki.v2.userRates.increment(id: "100")GraphQL API
1) Raw GraphQL
import Swiki
import SwikiModels
struct SearchVars: Encodable {
let search: String?
let limit: Int?
}
struct SearchResponse: Decodable {
struct AnimeItem: Decodable {
let id: String
let name: String
}
let animes: [AnimeItem]
}
let response: SearchResponse = try await swiki.graphQL.execute(
query: """
query SearchAnimes($search: String, $limit: PositiveInt) {
animes(search: $search, limit: $limit) { id name }
}
""",
operationName: "SearchAnimes",
variables: SearchVars(search: "bakemonogatari", limit: 3),
responseType: SearchResponse.self
)2) Typed operations
import Swiki
import SwikiModels
let operation = SwikiGraphQLOperations.DefaultUserRatesOperation(
variables: .init(
page: 1,
limit: 5,
userId: nil,
targetType: .anime,
status: nil,
orderField: .updatedAt,
sortOrder: .desc
)
)
let data = try await swiki.graphQL.execute(operation: operation)
print(data.userRates.count)GraphQL Operation Generation
Operations are stored in GraphQLOperations/*.graphql.
Generate with:
swift run SwikiGraphQLOperationGenerator \
--schema Sources/SwikiModels/schema.graphql \
--operations GraphQLOperations \
--output Sources/SwikiModels/GraphQLAfter generation:
Sources/SwikiModels/GraphQL/SwikiGraphQLOperations.generated.swift(namespace)Sources/SwikiModels/GraphQL/SwikiGraphQLOperations+<OperationName>.generated.swift(one file per operation)
Current default operations:
DefaultAnimesOperationDefaultMangasOperationDefaultCharactersOperationDefaultPeopleOperationDefaultUserRatesOperation
Model Typing
- All REST models are in
SwikiModels. - GraphQL generator is configured to reuse parts of
SwikiModels:
- enum types (SwikiAnimeKind, SwikiUserRateStatus, etc.), - SwikiIncompleteDate for IncompleteDate.
Limits and Headers
- Built-in request limiter by default:
5 RPSand90 RPM(can be disabled withisRpsRpmRestrictionsEnabled: false). - Added headers:
- User-Agent (from configuration), - Authorization: Bearer ... (if token is available), - X-Client-Id (if clientId/oauthCredentials.clientId is set), - any additionalHeaders.
Errors
- REST/GraphQL transport:
SwikiClientError - OAuth:
SwikiOAuthError - Keychain store:
SwikiKeychainOAuthTokenStoreError
Project Structure
Sources/Swiki- clients, transport, OAuth, configurationSources/SwikiModels- REST/GraphQL modelsSources/SwikiGraphQLOperationGenerator- GraphQL operation generator CLIGraphQLOperations- source.graphqloperationsTests/SwikiTests- tests
Useful Commands
swift build
swift test
swift run SwikiGraphQLOperationGenerator --helpExample SwiftUI Project
A ready-to-run example app is available in:
Examples/SwikiExampleApp
What the example demonstrates:
- OAuth authorization (
ASWebAuthenticationSession) - REST requests (
v1/users/whoami,v1/animes) - Typed GraphQL operation
Detailed run instructions:
Examples/SwikiExampleApp/README.md
License
MIT. See LICENSE.
Package Metadata
Repository: tixster/swiki
Default branch: main
README: README.md