734 lines
15 KiB
Markdown
Raw Permalink Normal View History

# nu_plugin_nickel
Nushell plugin for seamless Nickel configuration language integration. Load, evaluate, format, and validate Nickel files directly from Nushell scripts.
## Quick Start
```bash
# Build and register the plugin
just build-plugin nu_plugin_nickel
just register-plugin nu_plugin_nickel
# Verify installation
plugin list | where name == "nickel-export"
```
## Overview
5 commands for working with Nickel files:
| Command | Purpose |
|---------|---------|
| `nickel-export` | Export Nickel to JSON/YAML (with smart type conversion) |
| `nickel-eval` | Evaluate with automatic caching (for config loading) |
| `nickel-format` | Format Nickel files (in-place) |
| `nickel-validate` | Validate all Nickel files in a directory |
| `nickel-cache-status` | Show cache statistics |
## Core Concept: Smart Type Conversion
The plugin converts output intelligently based on whether you specify a format:
```
nickel-export config.ncl → Nushell object (parsed JSON)
nickel-export config.ncl -f json → Raw JSON string
nickel-export config.ncl -f yaml → Raw YAML string
```
**Why?** Default behavior gives you structured data for programming. Explicit `-f` gives you raw output for external tools.
---
## Complete Usage Guide
### 1. Load Configuration as Object (Most Common)
**Without `-f` flag → Returns Nushell object**
```nu
# Load configuration into a variable
let config = nickel-export workspace/config.ncl
# Access nested values with cell paths
$config.database.host # "localhost"
$config.database.port # 5432
$config.database.username # "admin"
# Work with arrays
$config.servers | length # 3
$config.servers | map {|s| $s.name}
# Filter and transform
$config.services
| where enabled == true
| each {|svc| {name: $svc.name, port: $svc.port}}
```
### 2. Get Raw Output (For External Tools)
**With `-f` flag → Returns raw string**
```nu
# Export as raw JSON string
let json = nickel-export config.ncl -f json
$json | save output.json
# Export as raw YAML string
nickel-export config.ncl -f yaml | save config.yaml
# Pipe to external tools
nickel-export config.ncl -f json | jq '.database'
nickel-export config.ncl -f json | curl -X POST -d @- http://api.example.com
```
### 3. Primary Config Loader with Caching
**`nickel-eval` is optimized for configuration loading**
```nu
# Load with automatic caching (80-90% hit rate)
let cfg = nickel-eval workspace/provisioning.ncl
# Works just like nickel-export (same smart conversion)
$cfg.infrastructure.cloud_provider # "aws"
$cfg.infrastructure.region # "us-east-1"
# Caching is transparent
nickel-eval config.ncl # First call: 100-200ms
nickel-eval config.ncl # Subsequent calls: 1-5ms
```
### 4. Format Nickel Files
```nu
# Format a single file (modifies in place)
nickel-format config.ncl
# Format multiple files
glob "**/*.ncl" | each {|f| nickel-format $f}
```
### 5. Validate Nickel Project
```nu
# Validate all .ncl files in directory
nickel-validate ./workspace/config
# Validate current directory
nickel-validate
# Output:
# ✅ workspace/config/main.ncl
# ✅ workspace/config/lib.ncl
# ✅ workspace/config/vars.ncl
```
### 6. Check Cache Status
```nu
nickel-cache-status
# Output:
# ╭──────────────────────────────────────────────────╮
# │ cache_dir: ~/.cache/provisioning/config-cache/ │
# │ entries: 42 │
# │ enabled: true │
# ╰──────────────────────────────────────────────────╯
```
---
## Real-World Examples
### Example 1: Multi-Environment Configuration
```nu
# Load environment-specific config
let env = "production"
let config = nickel-eval "config/provisioning-($env).ncl"
# Use in deployment
def deploy [service: string] {
let svc_config = $config.services | where name == $service | first
print $"Deploying ($service) to ($svc_config.region)"
print $" Image: ($svc_config.docker.image)"
print $" Replicas: ($svc_config.replicas)"
print $" Port: ($svc_config.port)"
}
deploy "api-server"
```
### Example 2: Generate Kubernetes Manifests
```nu
# Load infrastructure config
let infra = nickel-eval "infrastructure.ncl"
# Generate K8s manifests
$infra.services | each {|svc|
{
apiVersion: "apps/v1"
kind: "Deployment"
metadata: {name: $svc.name}
spec: {
replicas: $svc.replicas
template: {
spec: {
containers: [{
name: $svc.name
image: $svc.docker.image
ports: [{containerPort: $svc.port}]
}]
}
}
}
}
}
| each {|manifest|
$manifest | to json | save "k8s/($manifest.metadata.name).yaml"
}
```
### Example 3: Configuration Validation Script
```nu
def validate-config [config-path: path] {
# Validate syntax
nickel-validate $config-path | print
# Load and check required fields
let config = nickel-eval $config-path
let required = ["database", "services", "infrastructure"]
$required | each {|field|
if ($config | has $field) {
print $"✅ ($field): present"
} else {
print $"❌ ($field): MISSING"
}
}
# Check configuration consistency
let db_replicas = $config.database.replicas
let svc_replicas = ($config.services | map {|s| $s.replicas} | math sum)
if $db_replicas >= $svc_replicas {
print "✅ Database replicas sufficient"
} else {
print "❌ WARNING: Services exceed database capacity"
}
}
validate-config "workspace/config.ncl"
```
### Example 4: Generate Configuration from Template
```nu
# Load base config template
let template = nickel-eval "templates/base.ncl"
# Customize for specific environment
let prod_config = {
environment: "production"
debug: false
replicas: ($template.replicas * 3)
services: ($template.services | map {|s|
$s | merge {
replicas: 5
resources: {
memory: "2Gi"
cpu: "1000m"
}
}
})
}
# Export as JSON
$prod_config | to json | save "production-config.json"
```
### Example 5: Merge Multiple Configurations
```nu
# Load base config
let base = nickel-eval "config/base.ncl"
# Load environment-specific overrides
let env_overrides = nickel-eval "config/($env).ncl"
# Load local customizations
let local = nickel-eval "config/local.ncl"
# Merge with precedence: local > env > base
let final_config = $base
| merge $env_overrides
| merge $local
print $"Final configuration:"
print $final_config
```
---
## Command Reference
### nickel-export
```nu
nickel-export <FILE> [-f FORMAT] [-o OUTPUT]
```
**Arguments:**
- `FILE` - Path to Nickel file (required)
**Flags:**
- `-f, --format` - Output format: `json` (default), `yaml` (optional)
- `-o, --output` - Save to file instead of returning value (optional)
**Return Type:**
- Without `-f`: Nushell object (Record/List)
- With `-f json`: Raw JSON string
- With `-f yaml`: Raw YAML string
**Examples:**
```nu
nickel-export config.ncl # → object
nickel-export config.ncl -f json # → JSON string
nickel-export config.ncl -f yaml -o out.yaml
```
### nickel-eval
```nu
nickel-eval <FILE> [-f FORMAT] [--cache]
```
**Arguments:**
- `FILE` - Path to Nickel file (required)
**Flags:**
- `-f, --format` - Output format: `json` (default), `yaml` (optional)
- `--cache` - Use caching (enabled by default, flag for future use)
**Return Type:**
- Without `-f`: Nushell object (with caching)
- With `-f json`: Raw JSON string (with caching)
- With `-f yaml`: Raw YAML string (with caching)
**Examples:**
```nu
nickel-eval workspace/config.ncl # → cached object
nickel-eval config.ncl -f json # → cached JSON string
let cfg = nickel-eval config.ncl
$cfg.database.host
```
### nickel-format
```nu
nickel-format <FILE>
```
**Arguments:**
- `FILE` - Path to Nickel file to format (required)
**Examples:**
```nu
nickel-format config.ncl
glob "**/*.ncl" | each {|f| nickel-format $f}
```
### nickel-validate
```nu
nickel-validate [DIR]
```
**Arguments:**
- `DIR` - Directory to validate (optional, defaults to current directory)
**Examples:**
```nu
nickel-validate ./workspace/config
nickel-validate
```
### nickel-cache-status
```nu
nickel-cache-status
```
Returns record with cache information:
- `cache_dir` - Cache directory path
- `entries` - Number of cached entries
- `enabled` - Whether caching is enabled
**Examples:**
```nu
nickel-cache-status
let cache = nickel-cache-status
print $"Cache has ($cache.entries) entries at ($cache.cache_dir)"
```
---
## Type Conversion Details
### Without `-f` Flag (Object Mode)
The plugin converts Nickel output to Nushell types:
```
JSON Input → Nushell Type
─────────────────────────────────────
{"key": "value"} → {key: "value"}
[1, 2, 3] → [1, 2, 3]
"string" → "string"
123 → 123
true → true
null → null
```
Enables full Nushell data access:
```nu
let config = nickel-export config.ncl
# Cell path access
$config.database.host
# Filtering
$config.services | where enabled == true
# Transformation
$config.services | map {|s| {name: $s.name, port: $s.port}}
# Custom functions
def get-service [name: string] {
$config.services | where name == $name | first
}
```
### With `-f` Flag (Raw Mode)
Returns unprocessed string:
```nu
nickel-export config.ncl -f json
# Returns: "{\"database\":{\"host\":\"localhost\", ...}}"
nickel-export config.ncl -f yaml
# Returns: "database:\n host: localhost\n ..."
```
Use for:
- Saving to files with specific format
- Piping to external JSON/YAML tools
- API calls requiring raw format
- Integration with non-Nushell tools
---
## Caching
### Automatic Caching with `nickel-eval`
Results are cached using content-addressed storage:
```
Cache location: ~/.cache/provisioning/config-cache/
Cache key: SHA256(file_content + format)
First call: ~100-200ms
Cached calls: ~1-5ms
```
**Characteristics:**
- Non-blocking (errors are silently ignored)
- Transparent (no configuration needed)
- Hit rate: ~80-90% in typical workflows
- Per-format caching (json and yaml cached separately)
**Manual cache inspection:**
```nu
let status = nickel-cache-status
print $"Cache entries: ($status.entries)"
print $"Cache location: ($status.cache_dir)"
# List cache files
ls ($status.cache_dir)
```
---
## Troubleshooting
### Plugin Not Found
```
Error: Unknown command 'nickel-export'
```
**Solution:**
```bash
# Register the plugin
just register-plugin nu_plugin_nickel
# Verify registration
plugin list | grep nickel
```
### Nickel Binary Not Found
```
Error: Nickel execution failed: No such file or directory
```
**Solution:**
Ensure `nickel` CLI is installed and in PATH:
```bash
# Check if nickel is available
which nickel
# Install nickel (macOS)
brew install nickel-lang/nickel/nickel
# Or build from source
cargo install nickel
```
### File Not Found
```
Error: Nickel file not found: config.ncl
```
**Solution:**
Use absolute path or verify file exists:
```nu
# Use absolute path
nickel-export /absolute/path/to/config.ncl
# Verify file exists
ls config.ncl
```
### Cache Issues
```
# Clear cache if needed
rm -rf ~/.cache/provisioning/config-cache/*
# Check cache status
nickel-cache-status
```
### JSON Parsing Error
If `-f json` returns parsing error, the Nickel file may not export valid JSON:
```nu
# Test with raw Nickel output
nickel-export config.ncl -f json | print
```
---
## Architecture
### Design Pattern: CLI Wrapper
The plugin uses an elegant **CLI wrapper** pattern:
```
Nushell Script
nickel-export/eval command
Command::new("nickel")
Nickel official CLI
Module resolution (guaranteed correct)
JSON/YAML output
Smart type conversion
Nushell object or raw string
```
**Benefits:**
- ✅ Module resolution guaranteed correct (official CLI)
- ✅ Works with all Nickel versions automatically
- ✅ All Nickel CLI features automatically supported
- ✅ Zero maintenance burden
**Trade-offs:**
- ⚠️ Requires `nickel` binary in PATH
- ⚠️ ~100-200ms per evaluation (mitigated by caching)
### Type Conversion Flow
```rust
nickel export /file.ncl --format json
Captures stdout (JSON string)
serde_json::from_str (parse)
json_value_to_nu_value (convert recursively)
├── Object → Record
├── Array → List
├── String → String
├── Number → Int or Float
├── Boolean → Bool
└── Null → Nothing
Returns nu_protocol::Value
Nushell receives properly typed data
```
---
## Integration Examples
### With Provisioning System
```nu
# Load provisioning config
let prov = nickel-eval "workspace/provisioning.ncl"
# Deploy infrastructure
def deploy [] {
for region in $prov.regions {
print $"Deploying to ($region.name)..."
# Your deployment logic
}
}
# Validate configuration before deploy
def validate [] {
nickel-validate "workspace/provisioning.ncl"
}
```
### In Nushell Configuration
```nu
# env.nu or config.nu
# Load environment from Nickel
let env-config = nickel-eval "~/.config/nushell/environment.ncl"
# Set environment variables
$env.MY_VAR = $env-config.my_var
$env.DATABASE_URL = $env-config.database.url
```
### In CI/CD Pipelines
```nu
# GitHub Actions / GitLab CI script
# Load config
let config = nickel-eval ".provisioning/ci-config.ncl"
# Check if tests should run
if $config.run_tests {
print "Running tests..."
}
# Set deployment target
export DEPLOY_TARGET = $config.deployment.target
```
---
## Performance Tips
1. **Use `nickel-eval` for repeated access**
```nu
# ❌ Bad: 3 separate evaluations
print ($config.database | nickel-eval)
print ($config.services | nickel-eval)
# ✅ Good: Single evaluation, cached
let cfg = nickel-eval config.ncl
print $cfg.database
print $cfg.services
```
2. **Avoid format conversion loops**
```nu
# ❌ Bad: Converts each time
(1..100) | each {|i| nickel-export config.ncl | ...}
# ✅ Good: Convert once
let cfg = nickel-eval config.ncl
(1..100) | each {|i| ... $cfg ...}
```
3. **Use raw output for large datasets**
```nu
# ❌ Bad: Large object in memory
let big = nickel-export huge-config.ncl
# ✅ Good: Stream raw JSON
nickel-export huge-config.ncl -f json | jq '.items[]'
```
---
## Requirements
- **Nushell**: 0.110.0 or later
- **Nickel CLI**: Latest version (install via `brew` or `cargo`)
- **Rust**: For building the plugin (if not using pre-built binary)
---
## Building from Source
```bash
cd nu_plugin_nickel
cargo build --release
```
Binary will be at: `target/release/nu_plugin_nickel`
---
## Testing
```bash
# Run unit tests
cargo test
# Verify compilation
cargo check
# Run clippy linting
cargo clippy -- -D warnings
```
---
## License
MIT
---
## Further Reading
- [Nickel Official Documentation](https://nickel-lang.org/)
- [Nushell Plugin Development](https://www.nushell.sh/book/plugins.html)
- [Architecture Decision Record](./adr-001-nickel-cli-wrapper-architecture.md)