Skip to content

Repository files navigation

pklr

A pure Rust parser and evaluator for Apple's Pkl configuration language. No external binary or CLI required.

Features

  • Lexer, parser, and evaluator written entirely in Rust
  • Evaluates .pkl files to serde_json::Value
  • Import and amends resolution for local files
  • Glob imports, as import* "dir/*.pkl" as Mods declarations and import*("dir/*.pkl") expressions
  • Persistent caching, cache preloading, and offline evaluation for package:// imports
  • Concurrent prefetching of remote http(s):// and package:// imports
  • String interpolation, lambdas, higher-order methods
  • Rich error diagnostics via miette

Usage

use pklr::eval_to_json;

let json = eval_to_json(std::path::Path::new("config.pkl"))?;
println!("{}", json);

The API is synchronous. A default build uses the standard library for files and ureq for HTTP, and creates no Tokio runtime. Use EvaluatorBuilder::http_agent with a custom pklr::ureq::Agent for proxy, certificate, or timeout configuration.

Before evaluating a module, pklr prefetches its remote http(s):// and package:// imports, and theirs in turn, fetching each level in one batch. The native capabilities run up to eight requests at once. Prefetching only fills caches: a failed prefetch is retried when evaluation needs the module, so results and errors are the same as fetching one module at a time.

Cargo features

Feature Default What it adds
eval-core via the others The evaluator, without any host IO. Supply your own EvalCapabilities.
native-io yes NativeCapabilities, EvaluatorBuilder, eval_to_json, analyze_imports (std fs).
http yes HTTP imports and package downloads through ureq and rustls.
package-zip yes package:// zip archives.
miette-diagnostics yes Rich error diagnostics.
async no A reqwest HTTP backend on Tokio and async entry points.

An embedder with its own IO can depend on the evaluator alone:

pklr = { version = "5", default-features = false, features = ["eval-core"] }
let mut evaluator = pklr::Evaluator::with_capabilities(MyCapabilities::new());
let value = evaluator.eval_file(std::path::Path::new("config.pkl"))?;

EvalCapabilities methods are synchronous. A host with asynchronous IO can run the evaluator on a blocking thread and block on its own futures inside them. Override fetch_text_many and fetch_bytes_many to fetch prefetch batches concurrently; the defaults fetch one URL after another. Both get a FetchBudget (prefetching allows 64 MiB per evaluation) to charge response bytes to: start no request once it is spent, and drop a body that no longer fits.

Optional async support

The async feature is additive. It is for applications that already use reqwest, and it fetches prefetch batches concurrently on Tokio:

pklr = { version = "5", features = ["async"] }
let json = pklr::EvaluatorBuilder::new()
    .http_client(pklr::reqwest::Client::new())
    .eval_to_json_async(std::path::Path::new("config.pkl"))
    .await?;

eval_async, eval_to_json_async and the free pklr::eval_to_json_async run the synchronous evaluation on Tokio's blocking thread pool. With a reqwest client, requests run on the caller's runtime under block_in_place when called from a multi-threaded runtime, and on a private runtime otherwise, so the synchronous methods also work with a reqwest client.

Package downloads can be shared across evaluator instances and reused without network access:

fn load_config() -> pklr::Result<serde_json::Value> {
    pklr::EvaluatorBuilder::new()
        .package_cache_dir(".pklr-cache")
        .offline(true)
        .eval_to_json(std::path::Path::new("config.pkl"))
}

A host that already ships a copy of a package can seed the cache with it, so a config importing that package evaluates without any network round trip:

static PACKAGE: &[u8] = include_bytes!("pkg@1.0.0.zip");

fn load_bundled_config() -> pklr::Result<serde_json::Value> {
    pklr::EvaluatorBuilder::new()
        .package_cache_dir(".pklr-cache")
        .preload_package("https://example.com/pkg@1.0.0.zip", "zip", PACKAGE)
        .eval_to_json(std::path::Path::new("config.pkl"))
}

Cached content already on disk wins, so preloading never overrides a package fetched from the network, and a config pinning a different version still resolves that version normally.

License

MIT

About

Pure Rust pkl configuration language parser and evaluator

Resources

Stars

77 stars

Watchers

3 watching

Forks

Releases

Packages

Used by

Contributors

Languages