Typed Swift client for the Shikimori API (v1, v2, GraphQL) with OAuth2 support, automatic token refresh, and GraphQL operation generation.
- 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_codefor a token, - manual and automatic token refresh,
ASWebAuthenticationSessionon Apple platforms.
- exchange
- Token storage via
SwikiOAuthTokenStore:- Keychain by default on Apple platforms,
- custom storage for other platforms.
- GraphQL:
- raw queries,
- typed operations (
SwikiGraphQLOperation), - generate operations from
.graphqlfiles (one.swiftfile per operation).
- Swift
6.2+ - Platforms:
- iOS 16+
- macOS 13+
- tvOS 16+
- watchOS 9+
- Linux
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")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
)
)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)
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.
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)#if canImport(AuthenticationServices)
let token = try await swiki.oauth?.authorizeWithWebAuthenticationSession(
scopes: [.userRates, .comments, .topics]
)
#endifguard 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>")- Automatically:
- on
401, the request is retried afterrefreshTokenIfPossible(); - when a token expires,
validAccessToken()attempts refresh.
- on
- Manually:
let newToken = try await swiki.oauth?.refreshToken()- 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
}
}swiki.v1.<resource>
swiki.v2.<resource>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.
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
)
)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.
abuseRequests, episodeNotifications, topicIgnore, userIgnore, userRates.
// 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")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
)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)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
- All REST models are in
SwikiModels. - GraphQL generator is configured to reuse parts of
SwikiModels:- enum types (
SwikiAnimeKind,SwikiUserRateStatus, etc.), SwikiIncompleteDateforIncompleteDate.
- enum types (
- 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(ifclientId/oauthCredentials.clientIdis set),- any
additionalHeaders.
- REST/GraphQL transport:
SwikiClientError - OAuth:
SwikiOAuthError - Keychain store:
SwikiKeychainOAuthTokenStoreError
Sources/Swiki- clients, transport, OAuth, configurationSources/SwikiModels- REST/GraphQL modelsSources/SwikiGraphQLOperationGenerator- GraphQL operation generator CLIGraphQLOperations- source.graphqloperationsTests/SwikiTests- tests
swift build
swift test
swift run SwikiGraphQLOperationGenerator --helpA 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
MIT. See LICENSE.