acyka api

The libraries

Six of them, and they are the same library six times rather than six libraries.

# TypeScript
bun add @acyka/api

# Python
pip install acyka

# Rust
cargo add acyka

# Kotlin / JVM — build.gradle.kts
implementation("cc.acyka:acyka:1.0.0")

# C#
dotnet add package Acyka

# C++ — CMakeLists.txt, header-only
FetchContent_Declare(acyka
    GIT_REPOSITORY https://github.com/cykacloud/acyka-sdk.git
    GIT_TAG        v1.0.0
    SOURCE_SUBDIR  packages/cpp)
languagepackageruns on
TypeScript@acyka/apiNode 20+, Bun, Deno, Workers, browsers
Pythonacyka3.11+
Rustacyka1.75+, tokio
Kotlincc.acyka:acykaJVM 17, coroutines
C#Acykanet8.0, net10.0
C++acykaC++20, header-only

What is generated #

The types and one method per operation — 43 of them and 28 shapes, in every language. They come out of openapi.json, which the api writes from annotations that sit on its own handlers, and a test there drives every path in that document against the real router.

So none of the six can be behind the others, and none of them can describe an endpoint that does not exist. There is one reader shared by all six emitters, because six emitters each reading the document would be six places that have to agree about what allOf means — and they would not: the first language written would be right and the sixth would have a subtly different idea of which fields are optional.

What is written by hand, in each of them #

This is the half that decides whether a client is pleasant rather than merely correct.

  • The four OAuth flows. Authorization code with PKCE, the refresh that rotates, client_credentials, and the device flow.
  • A token that renews itself, with the refresh serialised through one lock. Four requests noticing one expiry must send one refresh between them: the tokens rotate, so the second would present one the first has retired — and the server can only read that as theft.
  • Backing off on the numbers the server actually sent, rather than guessing.
  • Paginators that stop on a short page rather than on total.
  • One error type per phrase name.
  • Verifying a webhook, over the raw bytes, in constant time, with the age checked.

And the parts that are about the language #

  • TypeScript. No dependencies at all — `fetch` and `crypto.subtle` are everywhere it runs.
  • Python. Frozen dataclasses with generated parsers, and a synchronous *and* an awaited client. One dependency, httpx.
  • Rust. Every read is a builder, and a paginator is a `Stream` that is already `Unpin`. rustls by default.
  • Kotlin. Suspending reads and a `Flow` for pages. Usable from Java too.
  • C#. Records, and `IAsyncEnumerable` for pages.
  • C++. One include. libcurl behind a seam, so a project that has already chosen an HTTP client can pass its own.

The wire stays snake_case #

Four of the six leave the field names exactly as they arrive, because shikimori_id is already their own convention. Kotlin and C# rename them — shikimoriId, ShikimoriId — and in those two the mapping is written on every single field rather than only where the two disagree: the day a field arrives already camelCase it would silently have no annotation, and nobody would notice until it did disagree.

The reference shows both, and switches with the language tab.

If something is missing #

Every library exposes its transport, so a route that exists and has not been generated yet is still reachable — and an issue at cykacloud/acyka-sdk is the way to say so.