NickelEval.jl

Julia FFI bindings for Nickel configuration language
Log | Files | Refs | README | LICENSE

commit 0133a50ffd040d3ad37dc8bb741881151d2df9af
parent ef49bfa252e6ffb6a345ba3f0d5e48c0511b25a2
Author: Erik Loualiche <eloualic@umn.edu>
Date:   Fri,  6 Feb 2026 09:37:13 -0600

Add documentation with DocumenterVitepress

- docs/: Documentation using Documenter.jl + DocumenterVitepress
- Quick start, typed evaluation, export, and FFI guides
- GitHub Actions workflow for documentation deployment
- CI workflow for testing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Diffstat:
A.github/workflows/CI.yml | 24++++++++++++++++++++++++
A.github/workflows/Documentation.yaml | 29+++++++++++++++++++++++++++++
Adocs/Project.toml | 4++++
Adocs/make.jl | 35+++++++++++++++++++++++++++++++++++
Adocs/src/index.md | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adocs/src/lib/public.md | 37+++++++++++++++++++++++++++++++++++++
Adocs/src/man/export.md | 135+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adocs/src/man/ffi.md | 112+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adocs/src/man/quickstart.md | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adocs/src/man/typed.md | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
10 files changed, 651 insertions(+), 0 deletions(-)

diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml @@ -0,0 +1,24 @@ +name: CI + +on: + push: + branches: + - main + tags: '*' + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@v2 + with: + version: '1' + - uses: julia-actions/cache@v2 + - name: Install Nickel + run: | + curl -L https://github.com/tweag/nickel/releases/download/1.7.0/nickel-linux-x86_64 -o /usr/local/bin/nickel + chmod +x /usr/local/bin/nickel + - name: Run tests + run: julia --project=. -e 'using Pkg; Pkg.instantiate(); Pkg.test()' diff --git a/.github/workflows/Documentation.yaml b/.github/workflows/Documentation.yaml @@ -0,0 +1,29 @@ +name: Documentation + +on: + push: + branches: + - main + tags: '*' + pull_request: + +jobs: + build: + permissions: + actions: write + contents: write + pull-requests: read + statuses: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@v2 + with: + version: '1' + - uses: julia-actions/cache@v2 + - name: Install dependencies + run: julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' + - name: Build and deploy + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: julia --project=docs/ docs/make.jl diff --git a/docs/Project.toml b/docs/Project.toml @@ -0,0 +1,4 @@ +[deps] +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +DocumenterVitepress = "4710194d-e776-4893-9690-8d956a29c365" +NickelEval = "a1b2c3d4-e5f6-7890-abcd-ef1234567890" diff --git a/docs/make.jl b/docs/make.jl @@ -0,0 +1,35 @@ +#!/usr/bin/env julia + +using NickelEval +using Documenter +using DocumenterVitepress + +makedocs( + format = MarkdownVitepress( + repo = "https://github.com/LouLouLibs/NickelEval", + ), + repo = Remotes.GitHub("LouLouLibs", "NickelEval"), + sitename = "NickelEval.jl", + modules = [NickelEval], + authors = "LouLouLibs Contributors", + pages=[ + "Home" => "index.md", + "Manual" => [ + "man/quickstart.md", + "man/typed.md", + "man/export.md", + "man/ffi.md", + ], + "Library" => [ + "lib/public.md", + ] + ] +) + +deploydocs(; + repo = "github.com/LouLouLibs/NickelEval", + target = "build", + devbranch = "main", + branch = "gh-pages", + push_preview = true, +) diff --git a/docs/src/index.md b/docs/src/index.md @@ -0,0 +1,65 @@ +# NickelEval.jl + +Julia bindings for the [Nickel](https://nickel-lang.org/) configuration language. + +## Features + +- **Evaluate Nickel code** directly from Julia +- **Native type conversion** to Julia types (`Dict`, `NamedTuple`, custom structs) +- **Export to multiple formats** (JSON, TOML, YAML) +- **High-performance FFI** mode using Rust bindings +- **Dot-access** for configuration records via `JSON.Object` + +## Installation + +```julia +using Pkg +Pkg.add(url="https://github.com/LouLouLibs/NickelEval") +``` + +**Prerequisite:** Install the Nickel CLI from [nickel-lang.org](https://nickel-lang.org/) + +## Quick Example + +```julia +using NickelEval + +# Simple evaluation +nickel_eval("1 + 2") # => 3 + +# Records with dot-access +config = nickel_eval("{ host = \"localhost\", port = 8080 }") +config.host # => "localhost" +config.port # => 8080 + +# Typed evaluation +nickel_eval("{ x = 1, y = 2 }", Dict{String, Int}) +# => Dict{String, Int64}("x" => 1, "y" => 2) + +# Export to TOML +nickel_to_toml("{ name = \"myapp\", version = \"1.0\" }") +# => "name = \"myapp\"\nversion = \"1.0\"\n" +``` + +## Why Nickel? + +[Nickel](https://nickel-lang.org/) is a configuration language designed to be: + +- **Programmable**: Functions, let bindings, and standard library +- **Typed**: Optional contracts for validation +- **Mergeable**: Combine configurations with `&` +- **Safe**: No side effects, pure functional + +NickelEval.jl lets you leverage Nickel's power directly in your Julia workflows. + +## Contents + +```@contents +Pages = [ + "man/quickstart.md", + "man/typed.md", + "man/export.md", + "man/ffi.md", + "lib/public.md", +] +``` diff --git a/docs/src/lib/public.md b/docs/src/lib/public.md @@ -0,0 +1,37 @@ +# Public API + +## Evaluation Functions + +```@docs +nickel_eval +nickel_eval_file +nickel_read +``` + +## Export Functions + +```@docs +nickel_export +nickel_to_json +nickel_to_toml +nickel_to_yaml +``` + +## FFI Functions + +```@docs +check_ffi_available +nickel_eval_ffi +``` + +## String Macro + +```@docs +@ncl_str +``` + +## Types + +```@docs +NickelError +``` diff --git a/docs/src/man/export.md b/docs/src/man/export.md @@ -0,0 +1,135 @@ +# Export to Config Formats + +NickelEval can export Nickel code to JSON, TOML, or YAML strings for generating configuration files. + +## JSON Export + +```julia +nickel_to_json("{ name = \"myapp\", port = 8080 }") +``` + +Output: +```json +{ + "name": "myapp", + "port": 8080 +} +``` + +## TOML Export + +```julia +nickel_to_toml("{ name = \"myapp\", port = 8080 }") +``` + +Output: +```toml +name = "myapp" +port = 8080 +``` + +## YAML Export + +```julia +nickel_to_yaml("{ name = \"myapp\", port = 8080 }") +``` + +Output: +```yaml +name: myapp +port: 8080 +``` + +## Generic Export Function + +Use `nickel_export` with the `format` keyword: + +```julia +nickel_export("{ a = 1 }"; format=:json) +nickel_export("{ a = 1 }"; format=:toml) +nickel_export("{ a = 1 }"; format=:yaml) +``` + +## Generating Config Files + +### Example: Generate Multiple Formats + +```julia +config = """ +{ + database = { + host = "localhost", + port = 5432, + name = "mydb" + }, + server = { + host = "0.0.0.0", + port = 8080 + }, + logging = { + level = "info", + file = "/var/log/app.log" + } +} +""" + +# Generate TOML config +write("config.toml", nickel_to_toml(config)) + +# Generate YAML config +write("config.yaml", nickel_to_yaml(config)) + +# Generate JSON config +write("config.json", nickel_to_json(config)) +``` + +### Example: Environment-Specific Configs + +```julia +base_config = """ +{ + app_name = "myapp", + log_level = "info" +} +""" + +dev_overrides = """ +{ + debug = true, + database = { host = "localhost" } +} +""" + +prod_overrides = """ +{ + debug = false, + database = { host = "db.production.com" } +} +""" + +# Merge and export +dev_config = nickel_export("$base_config & $dev_overrides"; format=:toml) +prod_config = nickel_export("$base_config & $prod_overrides"; format=:toml) +``` + +## Nested Structures + +TOML handles nested records as sections: + +```julia +nickel_to_toml(""" +{ + server = { + host = "0.0.0.0", + port = 8080 + } +} +""") +``` + +Output: +```toml +[server] +host = "0.0.0.0" +port = 8080 +``` diff --git a/docs/src/man/ffi.md b/docs/src/man/ffi.md @@ -0,0 +1,112 @@ +# FFI Mode (High Performance) + +For repeated evaluations, NickelEval provides native FFI bindings to a Rust library that wraps `nickel-lang-core`. This eliminates subprocess overhead. + +## Checking FFI Availability + +```julia +using NickelEval + +check_ffi_available() # => true or false +``` + +FFI is available when the compiled Rust library exists in the `deps/` folder. + +## Using FFI Evaluation + +```julia +# Basic evaluation +nickel_eval_ffi("1 + 2") # => 3 + +# With dot-access +config = nickel_eval_ffi("{ host = \"localhost\", port = 8080 }") +config.host # => "localhost" + +# Typed evaluation +nickel_eval_ffi("{ a = 1, b = 2 }", Dict{String, Int}) +# => Dict{String, Int64}("a" => 1, "b" => 2) +``` + +## Building the FFI Library + +### Requirements + +- Rust toolchain (install from [rustup.rs](https://rustup.rs)) +- Cargo + +### Build Steps + +```bash +cd rust/nickel-jl +cargo build --release +``` + +Then copy the library to `deps/`: + +```bash +# macOS +cp target/release/libnickel_jl.dylib ../../deps/ + +# Linux +cp target/release/libnickel_jl.so ../../deps/ + +# Windows +cp target/release/nickel_jl.dll ../../deps/ +``` + +## Performance Comparison + +FFI mode is faster for repeated evaluations because it: + +1. **No process spawn**: Direct library calls instead of subprocess +2. **Shared memory**: Values transfer directly without serialization +3. **Persistent state**: Library remains loaded + +For single evaluations, the difference is minimal. For batch processing or interactive use, FFI mode is significantly faster. + +## Binary Protocol + +The FFI uses a binary protocol that preserves type information: + +| Type Tag | Nickel Type | +|----------|-------------| +| 0 | Null | +| 1 | Bool | +| 2 | Int64 | +| 3 | Float64 | +| 4 | String | +| 5 | Array | +| 6 | Record | + +This allows direct conversion to Julia types without JSON parsing overhead. + +## Fallback Behavior + +If FFI is not available, you can still use the subprocess-based functions: + +```julia +# Always works (uses CLI) +nickel_eval("1 + 2") + +# Requires FFI library +nickel_eval_ffi("1 + 2") # Error if not built +``` + +## Troubleshooting + +### "FFI not available" Error + +Build the Rust library: + +```bash +cd rust/nickel-jl +cargo build --release +cp target/release/libnickel_jl.* ../../deps/ +``` + +### Library Not Found + +Ensure the library has the correct name for your platform: +- macOS: `libnickel_jl.dylib` +- Linux: `libnickel_jl.so` +- Windows: `nickel_jl.dll` diff --git a/docs/src/man/quickstart.md b/docs/src/man/quickstart.md @@ -0,0 +1,103 @@ +# Quick Start + +## Installation + +```julia +using Pkg +Pkg.add(url="https://github.com/LouLouLibs/NickelEval") +``` + +Make sure you have the Nickel CLI installed: +- macOS: `brew install nickel` +- Other: See [nickel-lang.org](https://nickel-lang.org/) + +## Basic Usage + +```julia +using NickelEval + +# Evaluate simple expressions +nickel_eval("1 + 2") # => 3 +nickel_eval("true") # => true +nickel_eval("\"hello\"") # => "hello" +``` + +## Working with Records + +Nickel records become `JSON.Object` with dot-access: + +```julia +config = nickel_eval(""" +{ + database = { + host = "localhost", + port = 5432 + }, + debug = true +} +""") + +config.database.host # => "localhost" +config.database.port # => 5432 +config.debug # => true +``` + +## Let Bindings and Functions + +```julia +# Let bindings +nickel_eval("let x = 10 in x * 2") # => 20 + +# Functions +nickel_eval(""" +let double = fun x => x * 2 in +double 21 +""") # => 42 +``` + +## Arrays + +```julia +nickel_eval("[1, 2, 3]") # => [1, 2, 3] + +# Array operations with std library +nickel_eval("[1, 2, 3] |> std.array.map (fun x => x * 2)") +# => [2, 4, 6] +``` + +## Record Merge + +```julia +nickel_eval("{ a = 1 } & { b = 2 }") +# => JSON.Object with a=1, b=2 +``` + +## String Macro + +For inline Nickel code: + +```julia +ncl"1 + 1" # => 2 + +config = ncl"{ host = \"localhost\" }" +config.host # => "localhost" +``` + +## File Evaluation + +```julia +# Evaluate a .ncl file +config = nickel_eval_file("config.ncl") +``` + +## Error Handling + +```julia +try + nickel_eval("{ x = }") # syntax error +catch e + if e isa NickelError + println("Error: ", e.message) + end +end +``` diff --git a/docs/src/man/typed.md b/docs/src/man/typed.md @@ -0,0 +1,107 @@ +# Typed Evaluation + +NickelEval supports converting Nickel values directly to typed Julia values using `JSON.jl 1.0`'s native typed parsing. + +## Basic Types + +```julia +nickel_eval("42", Int) # => 42 +nickel_eval("3.14", Float64) # => 3.14 +nickel_eval("\"hi\"", String) # => "hi" +nickel_eval("true", Bool) # => true +``` + +## Typed Dictionaries + +### String Keys + +```julia +result = nickel_eval("{ a = 1, b = 2 }", Dict{String, Int}) +# => Dict{String, Int64}("a" => 1, "b" => 2) + +result["a"] # => 1 +``` + +### Symbol Keys + +```julia +result = nickel_eval("{ x = 1.5, y = 2.5 }", Dict{Symbol, Float64}) +# => Dict{Symbol, Float64}(:x => 1.5, :y => 2.5) + +result[:x] # => 1.5 +``` + +## Typed Arrays + +```julia +nickel_eval("[1, 2, 3]", Vector{Int}) +# => [1, 2, 3] + +nickel_eval("[\"a\", \"b\", \"c\"]", Vector{String}) +# => ["a", "b", "c"] +``` + +## NamedTuples + +For structured configuration access: + +```julia +config = nickel_eval(""" +{ + host = "localhost", + port = 8080, + debug = true +} +""", @NamedTuple{host::String, port::Int, debug::Bool}) + +# => (host = "localhost", port = 8080, debug = true) + +config.host # => "localhost" +config.port # => 8080 +config.debug # => true +``` + +## Custom Structs + +Define your own types: + +```julia +struct ServerConfig + host::String + port::Int + workers::Int +end + +config = nickel_eval(""" +{ + host = "0.0.0.0", + port = 3000, + workers = 4 +} +""", ServerConfig) + +# => ServerConfig("0.0.0.0", 3000, 4) +``` + +## File Evaluation with Types + +```julia +# config.ncl: +# { environment = "production", max_connections = 100 } + +Config = @NamedTuple{environment::String, max_connections::Int} +config = nickel_eval_file("config.ncl", Config) + +config.environment # => "production" +config.max_connections # => 100 +``` + +## The `nickel_read` Alias + +`nickel_read` is an alias for typed `nickel_eval`: + +```julia +nickel_read("{ a = 1 }", Dict{String, Int}) +# equivalent to +nickel_eval("{ a = 1 }", Dict{String, Int}) +```