primate 0.1.4

A small DSL for cross-language constants. Write once, generate typed Rust, TypeScript, and Python.
Documentation

primate

CI Docs Crates.io License

primate is a small DSL and code generator for cross-language constants. You declare your shared values once, and it spits out idiomatic, typed Rust, TypeScript, and Python.

Setup

  1. Install:

    cargo install primate --locked
    
  2. Drop a primate.toml at the project root pointing at a directory of .prim files and listing your targets:

    input = "constants"
    
    [[output]]
    generator = "typescript"
    path      = "web/src/generated/constants/"
    
    [[output]]
    generator = "rust"
    path      = "src/generated/constants.rs"
    
  3. Write some constants:

    // constants/limits.prim
    
    /// Maximum upload size for a single request.
    u64 MAX_UPLOAD_SIZE = 100MiB
    
    /// Severity level. Integer-backed for fast filtering.
    enum LogLevel: u8 {
        Debug = 0,
        Info  = 1,
        Warn  = 2,
        Error = 3,
    }
    
    LogLevel DEFAULT_LEVEL = Info
    
  4. Run primate build and you'll get something like this:

    // src/generated/constants.rs
    pub mod limits {
        pub const MAX_UPLOAD_SIZE: u64 = 104857600;
    
        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
        #[repr(i32)]
        pub enum LogLevel { Debug = 0, Info = 1, Warn = 2, Error = 3 }
    
        pub const DEFAULT_LEVEL: LogLevel = LogLevel::Info;
    }
    
    // web/src/generated/constants/limits.ts
    export enum LogLevel { Debug = 0, Info = 1, Warn = 2, Error = 3 }
    export const maxUploadSize = 104857600 as const;
    export const defaultLevel = LogLevel.Info as const;
    

    Add a [[output]] block for python and you'll get a parallel package with IntEnum and timedelta in the right places.

Motivation

If you ship code to two or more language ecosystems, you've probably written the same constants more than once. The Node service has its own MAX_UPLOAD_SIZE, the Rust worker has another, the Python script has a third. They drift, one ends up wrong, and the bug shows up at 2am.

The usual fixes are awkward. A JSON config file gives up types and docs. A shared package only works when the languages can interop. Manually keeping things in sync works exactly until it doesn't.

primate takes a different angle: declare constants in one place, generate them in each target's idioms. Durations end up as std::time::Duration in Rust, Temporal.Duration (or milliseconds) in TypeScript, timedelta in Python. Integer-backed enums become #[repr], TS enum, and IntEnum. Doc comments follow the values to every callsite. Suffixed numeric literals — 30s, 100MiB, 5%, 1w — are bounds-checked at parse time, then become normal numbers in the generated output.

A canonical formatter (primate fmt) and an LSP server with diagnostics, hover, go-to-definition, find-references, and contextual completion ship with the binary. If you need a target the built-ins don't cover, there's a stdin/stdout plugin protocol.

Editor support

Documentation

Full docs at valtyr.github.io/primate, or build locally:

cd docs && mdbook serve --open

License

MIT — see LICENSE.