Skip to content

Latest commit

 

History

History
141 lines (101 loc) · 5.64 KB

File metadata and controls

141 lines (101 loc) · 5.64 KB

Architecture

lineark is four crates that form a clean pipeline:

  • lineark-codegen reads Linear API's GraphQL schema and generates typed Rust code into the SDK
  • lineark-sdk combines a small hand-written core with the generated types, queries, and mutations into a cohesive and reusable client library
  • lineark-derive provides #[derive(GraphQLFields)] — a proc macro for custom lean types with zero overfetching (re-exported by lineark-sdk, no extra dependency needed)
  • lineark consumes the SDK as a normal library — zero GraphQL, typed method calls with custom lean types via #[derive(GraphQLFields)]

The Big Picture

graph TD
    subgraph "Schema Sources"
        Schema["schema.graphql<br/><i>Linear's GraphQL SDL</i>"]
        Ops["operations.toml<br/><i>allowlist</i>"]
    end

    Codegen["lineark-codegen"]
    Schema & Ops --> Codegen

    Derive["lineark-derive<br/><i>#[derive(GraphQLFields)]</i>"]

    subgraph SDK["lineark-sdk"]
        Generated["generated/<br/><i>types, queries, mutations,<br/>client_impl</i>"]
        FieldSel["field_selection.rs<br/><i>GraphQLFields trait</i>"]
    end

    Codegen -- "writes .rs files" --> Generated
    Derive -- "proc macro" --> SDK

    CLI["lineark CLI"]
    CLI -- "client.issues().filter(f).send()" --> SDK

    API["Linear GraphQL API"]
    SDK -- "HTTP POST" --> API
Loading

Code Generation Pipeline

The codegen crate parses Linear's GraphQL schema (vendored as schema.graphql) using apollo-parser, then emits seven Rust source files into the SDK:

graph TD
    SDL["schema.graphql<br/><i>Linear's GraphQL SDL</i>"]
    TOML["operations.toml<br/><i>allowlist</i>"]

    SDL & TOML --> Parse["parser::parse()<br/><i>apollo-parser CST → simplified structs</i>"]

    Parse --> Always & Gated

    subgraph "Always generated (full schema)"
        Always["emit_scalars · emit_enums<br/>emit_types · emit_inputs"]
    end

    subgraph "Gated by operations.toml"
        Gated["emit_queries · emit_mutations"]
    end

    Always --> Out1["scalars.rs · enums.rs<br/>types.rs · inputs.rs"]
    Gated --> Out2["queries.rs · mutations.rs<br/>client_impl.rs"]

    Out1 & Out2 --> FMT["prettyplease + cargo fmt"]
    FMT --> Dir["lineark-sdk/src/generated/"]
Loading

Types, enums, scalars, and inputs are always fully generated from the schema. Queries and mutations are gated by operations.toml — only explicitly listed operations get code emitted. This keeps the SDK surface incremental and intentional.

SDK Structure

The SDK has a small hand-written core and a large generated layer:

graph TD
    Consumer["Your code calls:<br/><b>client.issues().filter(f).first(50).send()</b>"]

    Consumer --> Impl

    subgraph lineark-sdk
        Impl["client_impl.rs<br/><i>impl Client — thin delegation<br/>generated by codegen</i>"]
        Impl --> QM

        subgraph "Generated by codegen"
            QM["queries.rs / mutations.rs<br/><i>generic fns + builders<br/>T: DeserializeOwned + GraphQLFields</i>"]
            Types["types.rs / inputs.rs / enums.rs<br/><i>Issue, Team, IssueCreateInput,<br/>IssueFilter, IssuePriority, ...<br/>+ impl GraphQLFields for each type</i>"]
        end

        QM -- "uses types for<br/>args and return values" --> Types
        QM --> Client

        subgraph "Hand-written (~200 LOC)"
            Client["client.rs — Client struct<br/><i>execute() · execute_connection()</i>"]
            Support["auth.rs · error.rs · pagination.rs<br/>field_selection.rs"]
        end

        Client --> Support
    end

    Client -- "HTTP POST" --> API["Linear GraphQL API"]
Loading

The key trick: Client is defined in hand-written client.rs, but codegen adds methods to it via a separate impl Client block in client_impl.rs. Rust's open impl blocks make this seamless — consumers see one unified Client type with both hand-written and generated methods.

All query/mutation methods are generic over T: DeserializeOwned + GraphQLFields. The generated types implement GraphQLFields automatically (via codegen), and consumers can define custom lean structs with #[derive(GraphQLFields)] to fetch only the fields they need — the derive macro is re-exported by lineark-sdk, so use lineark_sdk::GraphQLFields gives you both the trait and the macro with no extra dependency. Custom types must include #[graphql(full_type = X)] pointing to the corresponding generated type — this is required for the query's type constraint and also validates fields at compile time.

How the CLI Plugs In

The CLI is a pure consumer of the SDK. It has zero GraphQL strings and uses custom lean types with #[derive(GraphQLFields)] to fetch only the fields it needs:

sequenceDiagram
    participant User
    participant CLI as lineark CLI
    participant SDK as lineark-sdk
    participant API as Linear API

    User->>CLI: lineark issues list --team ENG --mine
    CLI->>CLI: Parse args (clap)
    CLI->>SDK: resolve_team_id("ENG")
    SDK->>API: teams(first: 250)
    API-->>SDK: Connection<Team>
    SDK-->>CLI: team UUID

    CLI->>CLI: Build IssueFilter via serde_json
    CLI->>SDK: client.issues::<IssueRow>().filter(f).first(50).send()
    SDK->>SDK: Build GraphQL query + variables
    SDK->>API: POST /graphql
    API-->>SDK: JSON response
    SDK-->>SDK: Deserialize into Connection<IssueRow>
    SDK-->>CLI: Connection<IssueRow>

    CLI->>CLI: Format as table or JSON
    CLI-->>User: Output
Loading

Each CLI command module follows the same pattern:

  1. Parse command-line args
  2. Resolve human-friendly names to UUIDs (teams, users, labels, projects, cycles, issues)
  3. Call SDK builder methods
  4. Format output (tables for terminal, JSON when piped)