Jargon
Define your CLI jargon with JSON Schema.
A Crystal library that generates CLI interfaces from JSON Schema definitions. Define your data structure once in JSON Schema, get a CLI parser with validation for free.
Features
- Validation: Required fields, enum values, strict type checking,
additionalProperties - Standalone validator: Use
Jargon::Validatorto validate data without the CLI parser - Defaults: Schema defaults, config file defaults, environment variables
- Config files: Load from
.config/(XDG spec) with deep merge support - Help text: Generated from schema descriptions
- Auto help flags:
--helpand-hdetected automatically - Shell completions: Generate completion shims for bash, zsh, and fish, with optional app-driven dynamic completers for live data
- Positional args: Non-flag arguments assigned by position and variadic support.
- Short flags: Single-character flag aliases (
-v,-n 5) - Boolean flags: Support both
--verboseand--verbose falsestyles - Subcommands: Named sub-parsers with independent schemas (supports abbreviated invocations)
- Default subcommand: Fall back to a subcommand when none specified
- Stdin JSON: Read arguments as JSON from stdin with
- - Typo suggestions: "Did you mean?" for mistyped options
- $ref support: Reuse definitions with
$ref: "#/$defs/typename" - Extension annotations:
x-*keys pass through untouched for GUI/tooling consumers
Installation
Add the dependency to your shard.yml:
dependencies:
jargon:
github: trans/jargon
Then run shards install.
Usage
require "jargon"
# Define your schema
schema = %({
"type": "object",
"properties": {
"name": {"type": "string", "description": "User name"},
"age": {"type": "integer"},
"verbose": {"type": "boolean"}
},
"required": ["name"]
})
# Create CLI and run
cli = Jargon.cli("myapp", json: schema)
cli.run do |result|
puts result.to_pretty_json
end
The run method automatically handles:
--help/-h: prints help and exits--completions <shell>: prints shell completion script and exits- Validation errors: prints errors to STDERR and exits with code 1
YAML Schemas
YAML schemas are supported directly:
# schema.yaml
type: object
properties:
name:
type: string
description: User name
verbose:
type: boolean
short: v
required:
- name
schema = File.read("schema.yaml")
cli = Jargon.cli("myapp", yaml: schema)
Argument Styles
Three styles are supported interchangeably:
# Equals style (minimal)
myapp name=John age=30 verbose=true
# Colon style
myapp name:John age:30 verbose:true
# Traditional style
myapp --name John --age 30 --verbose
Mix and match as you like:
myapp name=John --age 30 verbose:true
Nested Objects
Use dot notation for nested properties:
schema = %({
"type": "object",
"properties": {
"user": {
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string"}
}
}
}
})
cli = Jargon.cli("myapp", json: schema)
result = cli.parse(["user.name=John", "[email protected]"])
# => {"user": {"name": "John", "email": "[email protected]"}}
Supported Types
| JSON Schema Type | CLI Example | Notes |
|------------------|-------------|-------|
| string | name=John | Default type |
| integer | count=42 | Parsed as Int64, strict validation |
| number | rate=3.14 | Parsed as Float64, strict validation |
| boolean | verbose=true or --verbose | Flag style supported |
| array | tags=a,b,c | Comma-separated |
| object | user.name=John | Dot notation |
Validation Constraints
Standard JSON Schema validation keywords are supported:
{
"properties": {
"port": {"type": "integer", "minimum": 1, "maximum": 65535},
"ratio": {"type": "number", "exclusiveMinimum": 0, "exclusiveMaximum": 1},
"password": {"type": "string", "minLength": 8, "maxLength": 64},
"email": {"type": "string", "format": "email"},
"website": {"type": "string", "format": "uri"},
"config": {"type": "string", "format": "path"},
"level": {"type": "string", "enum": ["debug", "info", "warn", "error"]},
"files": {"type": "array", "minItems": 1, "maxItems": 10, "uniqueItems": true},
"apiVersion": {"type": "string", "const": "v1"},
"tags": {
"type": "array",
"items": {"type": "string", "enum": ["alpha", "beta", "stable"]}
}
}
}
minimum/maximum: numeric range (inclusive)exclusiveMinimum/exclusiveMaximum: numeric range (exclusive)multipleOf: value must be divisible by this numberminLength/maxLength: string lengthminItems/maxItems: array lengthuniqueItems: no duplicate values in arraypattern: regex validation for stringsformat: semantic formats (email,uri,uuid,date,time,date-time,ipv4,ipv6,hostname,path). Thepathformat expands~to the user's home directoryenum: allowed values (works for array items too)const: exact value matchadditionalProperties: whenfalse, rejects unknown keys in objectsservice: whentrue, marks the command as a long-running service (UI hint for consumers)x-*: any key starting withx-is preserved verbatim as an extension annotation (see Extension Annotations)
Boolean Flags
Boolean flags support multiple styles:
# Flag style (sets to true)
myapp --verbose
# Explicit value
myapp --verbose true
myapp --verbose false
myapp --enabled no
# Equals style
myapp verbose=true
myapp --verbose=false
Recognized boolean values: true/false, yes/no, on/off, 1/0 (case-insensitive).
When a boolean flag is followed by a non-boolean value, the value is not consumed:
# --verbose is true, output.txt is a positional arg
myapp --verbose output.txt
Strict Numeric Validation
Invalid numeric values produce clear error messages:
$ myapp --count abc
Error: Invalid integer value 'abc' for count
$ myapp --count 10x
Error: Invalid integer value '10x' for count
Typo Suggestions
Mistyped options get helpful "did you mean?" suggestions:
$ myapp --verbos
Error: Unknown option '--verbos'. Did you mean '--verbose'?
$ myapp --formt json
Error: Unknown option '--formt'. Did you mean '--format'?
Positional Arguments
Define positional arguments with the positional array:
schema = %({
"type": "object",
"positional": ["file", "output"],
"properties": {
"file": {"type": "string", "description": "Input file"},
"output": {"type": "string", "description": "Output file"},
"verbose": {"type": "boolean"}
},
"required": ["file"]
})
cli = Jargon.cli("myapp", json: schema)
result = cli.parse(["input.txt", "output.txt", "--verbose"])
# => {"file": "input.txt", "output": "output.txt", "verbose": true}
myapp input.txt output.txt --verbose
Variadic Positionals
When the last positional has type: array, it collects all remaining arguments:
schema = %({
"type": "object",
"positional": ["files"],
"properties": {
"files": {"type": "array", "description": "Input files"},
"number": {"type": "boolean", "short": "n"}
}
})
cli = Jargon.cli("cat", json: schema)
result = cli.parse(["-n", "a.txt", "b.txt", "c.txt"])
# => {"number": true, "files": ["a.txt", "b.txt", "c.txt"]}
cat -n a.txt b.txt c.txt
Note: Flags should come before variadic positionals. Collection stops at the first flag encountered.
End of Options (--)
A bare -- marks the end of options (POSIX convention). Everything after it is treated as a positional argument, even if it looks like a flag — useful for passing flags through to another program. The -- marker itself is discarded, and a variadic positional captures the rest verbatim:
schema = %({
"type": "object",
"positional": ["argv"],
"properties": {
"verbose": {"type": "boolean", "short": "v"},
"argv": {"type": "array"}
}
})
cli = Jargon.cli("run", json: schema)
result = cli.parse(["--verbose", "--", "ls", "-la", "--color"])
# => {"verbose": true, "argv": ["ls", "-la", "--color"]}
Options before -- parse normally; only the first -- is special (a second one is a literal operand).
Short Flags
Define short flag aliases with the short property:
schema = %({
"type": "object",
"properties": {
"verbose": {"type": "boolean", "short": "v"},
"count": {"type": "integer", "short": "n"},
"output": {"type": "string", "short": "o"}
}
})
cli = Jargon.cli("myapp", json: schema)
result = cli.parse(["-v", "-n", "5", "-o", "out.txt"])
# => {"verbose": true, "count": 5, "output": "out.txt"}
myapp -v -n 5 -o out.txt
myapp --verbose --count 5 --output out.txt # equivalent
Help Flags
Jargon automatically detects --help and -h flags. When using run, help is printed and the program exits automatically:
cli = Jargon.cli("myapp", json: schema)
cli.run do |result|
# This block only runs if --help was NOT passed
puts result.to_pretty_json
end
myapp --help # top-level help
myapp -h # same
myapp fetch --help # subcommand help
myapp config set -h # nested subcommand help
If you need manual control, use parse instead:
result = cli.parse(ARGV)
if result.help_requested?
if subcmd = result.help_subcommand
puts cli.help(subcmd)
else
puts cli.help
end
exit 0
end
If you define a help property or use -h as a short flag for something else, Jargon won't intercept those flags:
# User-defined help property takes precedence
schema = %({
"type": "object",
"properties": {
"help": {"type": "string", "description": "Help topic"},
"host": {"type": "string", "short": "h"}
}
})
cli = Jargon.cli("myapp", json: schema)
result = cli.parse(["--help", "topic"])
result.help_requested? # => false
result["help"].as_s # => "topic"
result = cli.parse(["-h", "localhost"])
result["host"].as_s # => "localhost"
Shell Completions
Jargon can generate shell completion scripts for bash, zsh, and fish. When using run, the --completions <shell> flag is handled automatically.
The generated scripts are small, fixed shims: on every Tab they forward the cursor position and typed words back to your program (a hidden __complete verb that run and handle_completion recognize), and the program computes the candidates from the schema. This means one code path serves both static completion (subcommand names, flags, enum values) and dynamic completion (live data only your app knows) — and the installed script never needs regenerating when your CLI changes.
Installing Completions
Generate the completion shim once and save it to your shell's completions directory:
# Bash
myapp --completions bash > ~/.local/share/bash-completion/completions/myapp
# Zsh (ensure ~/.zfunc is in your fpath)
myapp --completions zsh > ~/.zfunc/_myapp
# Fish
myapp --completions fish > ~/.config/fish/completions/myapp.fish
Out of the box you get completion for subcommand names (including nested), long and short flags, and enum values — all derived from the schema, no extra code.
Dynamic Completions
For values the schema can't know — a document name, a tag, a key in a live store — register a completer against a field. It runs inside your program at completion time and returns the candidates:
cli = Jargon.cli("transfs", yaml: schema)
# Field path: the field name for a flat CLI, or `subcommand.field`
# (`a.b.field` when nested). Raises if the path isn't a real field.
cli.completer("forget.query") do |ctx|
store.search(ctx.partial).map(&.name) # candidates (Array(String))
end
cli.run { |result| ... } # `run` answers the __complete verb automatically
The block receives a Completion::Context:
| Field | Description |
|-------|-------------|
| ctx.partial | the token being completed (use it to scope your query) |
| ctx.subcommand | the resolved subcommand path, or nil at top level |
| ctx.arguments | a lenient parse of what's already typed, for filtering |
| ctx.words | the raw tokens, as an escape hatch |
A registered field completes dynamically; everything else stays static. You never write the __complete verb yourself — the shim supplies it.
Answering completion from a separate binary
Each Tab spawns whatever the shim calls. If your main program is heavy to start, point the shim at a small dedicated binary instead of paying full startup per keypress:
# generate a shim that calls `transfs-complete` rather than `transfs`
File.write(path, cli.bash_completion(command: "transfs-complete"))
# transfs-complete.cr — a minimal helper that only answers completion
cli = build_cli
cli.completer("forget.query") { |ctx| index.search(ctx.partial) }
exit cli.handle_completion # answers __complete and exits
handle_completion returns immediately for a normal invocation, so you can also call it at the very top of your existing main to answer completion before heavy initialization runs, without a second binary. (The separate-binary win only materializes if your candidate source has a light access path — an on-disk index, a cache, a daemon — rather than requiring the full app to boot.)
Manual Completion Handling
If you need manual control, use parse:
cli = Jargon.cli("myapp", json: schema)
result = cli.parse(ARGV)
if result.completion_requested?
case result.completion_shell
when "bash" then puts cli.bash_completion
when "zsh" then puts cli.zsh_completion
when "fish" then puts cli.fish_completion
end
exit 0
end
Subcommands
Create CLIs with subcommands, each with their own schema:
cli = Jargon.new("myapp")
cli.subcommand("fetch", json: %({
"type": "object",
"positional": ["url"],
"properties": {
"url": {"type": "string", "description": "Resource URL"},
"depth": {"type": "integer", "short": "d"}
},
"required": ["url"]
}))
cli.subcommand("save", json: %({
"type": "object",
"properties": {
"message": {"type": "string", "short": "m"},
"all": {"type": "boolean", "short": "a"}
},
"required": ["message"]
}))
cli.run do |result|
case result.subcommand
when "fetch"
url = result["url"].as_s
depth = result["depth"]?.try(&.as_i64)
when "save"
message = result["message"].as_s
all = result["all"]?.try(&.as_bool) || false
end
end
myapp fetch https://example.com/resource -d 1
myapp save -m "Updated config" -a
Nested Subcommands
Create nested subcommands by passing a CLI instance as the subcommand:
config = Jargon.new("config")
config.subcommand("set", json: %({
"type": "object",
"positional": ["key", "value"],
"properties": {
"key": {"type": "string"},
"value": {"type": "string"}
},
"required": ["key", "value"]
}))
config.subcommand("get", json: %({
"type": "object",
"positional": ["key"],
"properties": {
"key": {"type": "string"}
}
}))
cli = Jargon.new("myapp")
cli.subcommand("config", config)
cli.subcommand("status", json: %({"type": "object", "properties": {}}))
cli.run do |result|
case result.subcommand
when "config set"
key = result["key"].as_s
value = result["value"].as_s
when "config get"
key = result["key"].as_s
when "status"
# ...
end
end
myapp config set api_url https://api.example.com
myapp config get api_url
myapp status
The result.subcommand returns the full path as a space-separated string (e.g., "config set").
Default Subcommand
Set a default subcommand to use when no subcommand name is given:
cli = Jargon.new("xerp")
cli.subcommand("index", json: %({...}))
cli.subcommand("query", json: %({
"type": "object",
"positional": ["query_text"],
"properties": {
"query_text": {"type": "string"},
"top": {"type": "integer", "default": 10, "short": "n"}
}
}))
cli.default_subcommand("query")
# These are equivalent:
xerp query "search term" -n 5
xerp "search term" -n 5
Note: If the first argument matches a subcommand name, it's treated as a subcommand, not as input to the default. Use the explicit form if you need to search for a term that matches a subcommand name.
Subcommand Abbreviations
Subcommands can be abbreviated to any unique prefix (minimum 3 characters):
$ myapp checkout main # full name
$ myapp check main # abbreviated (if unambiguous)
$ myapp che main # still works
$ myapp ch main # too short (< 3 chars) - error
$ myapp co main # ambiguous (commit? config?) - error
Global Options
Use Jargon.merge to add common options to all subcommands:
global = %({
"type": "object",
"properties": {
"verbose": {"type": "boolean", "short": "v", "description": "Verbose output"},
"config": {"type": "string", "short": "c", "description": "Config file path"}
}
})
cli = Jargon.new("myapp")
cli.subcommand("fetch", json: Jargon.merge(%({
"type": "object",
"positional": ["url"],
"properties": {
"url": {"type": "string"},
"depth": {"type": "integer", "short": "d"}
}
}), global))
cli.subcommand("sync", json: Jargon.merge(%({
"type": "object",
"properties": {
"force": {"type": "boolean", "short": "f"}
}
}), global))
myapp fetch https://example.com/data -v
myapp sync --force --config myconfig.json
Subcommand properties take precedence if there's a conflict with global properties.
File-Based Subcommands
Load subcommands from external files for cleaner organization:
cli = Jargon.new("myapp")
cli.subcommand("fetch", file: "schemas/fetch.yaml")
cli.subcommand("save", file: "schemas/save.json")
Or define all subcommands in a single multi-document file:
# commands.yaml
---
name: fetch
type: object
properties:
url: {type: string}
---
name: save
type: object
properties:
file: {type: string}
# Load as top-level subcommands
cli = Jargon.cli("myapp", file: "commands.yaml")
# or
cli = Jargon.new("myapp")
cli.subcommand(file: "commands.yaml")
Load multi-doc as nested subcommands by providing a parent name:
cli = Jargon.new("myapp")
cli.subcommand("config", file: "config_commands.yaml") # config get, config set, etc.
Multi-document format is auto-detected for json:, yaml:, and file: parameters. Each document must have a name field.
JSON uses relaxed JSONL (consecutive objects with whitespace):
{
"name": "fetch",
"type": "object",
"properties": {"url": {"type": "string"}}
}
{
"name": "save",
"type": "object",
"properties": {"file": {"type": "string"}}
}
Schema Mixins
Share properties across subcommands using standard JSON Schema $id, $ref, and allOf:
---
$id: global
properties:
verbose: {type: boolean, short: v}
config: {type: string, short: c}
---
$id: output
properties:
format: {type: string, enum: [json, yaml, csv]}
---
name: fetch
allOf:
- {$ref: global}
- properties:
url: {type: string}
---
name: export
allOf:
- {$ref: global}
- {$ref: output}
- properties:
file: {type: string}
- Schemas with
$id(noname) are mixins - not registered as subcommands $refinallOfresolves to mixins defined in the same file- Properties are merged;
type: objectis inferred if missing - Subcommands explicitly opt-in via
allOf
This approach uses standard JSON Schema keywords while keeping mixin definitions alongside subcommands in a single file.
JSON from Stdin
Use - to read JSON input from stdin:
# JSON with subcommand field
echo '{"subcommand": "query", "query_text": "search term", "top": 5}' | xerp -
# JSON args for explicit subcommand
echo '{"result_id": "abc123", "useful": true}' | xerp mark -
If no subcommand field is present in xerp -, the default subcommand is used (if set).
The field name is configurable:
cli.subcommand_key("op") # default is "subcommand"
echo '{"op": "query", "query_text": "search"}' | xerp -
Environment Variables
Map schema properties to environment variables with the env property:
schema = %({
"type": "object",
"properties": {
"api-key": {"type": "string", "env": "MY_APP_API_KEY"},
"host": {"type": "string", "env": "MY_APP_HOST", "default": "localhost"},
"debug": {"type": "boolean", "env": "MY_APP_DEBUG"}
}
})
cli = Jargon.cli("myapp", json: schema)
cli.run do |result|
# result contains api-key, host from env, debug from CLI
end
export MY_APP_API_KEY=secret123
export MY_APP_HOST=prod.example.com
myapp --debug # api-key and host from env, debug from CLI
Merge order (highest priority first):
- CLI arguments
- Environment variables
- Config file defaults
- Schema defaults
Config Files
Load configuration from standard XDG locations with load_config. Supports YAML and JSON:
cli = Jargon.cli("myapp", json: schema)
config = cli.load_config # Returns JSON::Any or nil
cli.run(defaults: config) do |result|
# ...
end
Paths searched (first found wins, or merged if merge: true):
./.config/myapp.yaml/.yml/.json(project local)./.config/myapp/config.yaml/.yml/.json(project local, directory style)$XDG_CONFIG_HOME/myapp.yaml/.yml/.json(user global, typically~/.config)$XDG_CONFIG_HOME/myapp/config.yaml/.yml/.json(user global, directory style)
YAML is preferred over JSON when both exist at the same location.
By default, configs are deep-merged with project overriding user:
# Merge all found configs (default) - project wins over user
config = cli.load_config
# Or first-found wins
config = cli.load_config(merge: false)
Deep Merge
Nested objects are recursively merged, not overwritten:
# User config (~/.config/myapp.yaml)
database:
host: localhost
port: 5432
user: default_user
# Project config (.config/myapp.yaml)
database:
host: production.example.com
# Result after merge:
database:
host: production.example.com # from project
port: 5432 # preserved from user
user: default_user # preserved from user
Config Warnings
Invalid config files emit warnings to STDERR by default. To suppress:
Jargon.config_warnings = false
config = cli.load_config
Jargon.config_warnings = true
Example project config (.config/myapp.yaml):
host: localhost
port: 8080
debug: true
Or JSON (.config/myapp.json):
{
"host": "localhost",
"port": 8080,
"debug": true
}
The defaults: parameter accepts any JSON-like data, so you can load config however you prefer:
# From YAML
config = YAML.parse(File.read("config.yaml"))
cli.run(defaults: config) { |result| ... }
# From JSON
config = JSON.parse(File.read("settings.json"))
cli.run(defaults: config) { |result| ... }
Extension Annotations (x-*)
Schema keys starting with x- are treated as consumer-defined metadata,
following the JSON Schema / OpenAPI extension convention. Jargon preserves
them verbatim — on commands (doc level) and on individual properties — and
ignores them completely for parsing and validation. This gives downstream
consumers (a GUI, a docs generator, an AI tool layer) an open vocabulary of
hints carried by the same schema that drives the CLI:
---
name: forget
description: Remove a document from the store
x-ui:
destructive: true # GUI: confirm before running
properties:
query:
type: string
x-ui:
control: typeahead # GUI: render a search box, not a text field
source: documents
Annotations are exposed through the schema introspection API:
schema = cli.subcommands["forget"].as(Jargon::Schema)
schema.root.extensions # => {"x-ui" => {"destructive" => true}}
schema.root.extensions["x-ui"]? # => keys are stored exactly as written
query = schema.root.properties.not_nil!["query"]
query.extensions["x-ui"]["control"] # => "typeahead"
Jargon assigns no meaning to any x- key — define whatever vocabulary your
consumers need. (The built-in service hint predates this mechanism and
remains first-class.)
Standalone Validator
Use Jargon::Validator to validate data against a schema without the CLI parser. This is useful for validating JSON from APIs, config files, or other sources:
require "jargon"
schema = Jargon::Schema.from_json(%({
"type": "object",
"properties": {
"name": {"type": "string", "minLength": 1},
"age": {"type": "integer", "minimum": 0},
"role": {"type": "string", "enum": ["admin", "user"]}
},
"required": ["name"],
"additionalProperties": false
}))
data = {"name" => JSON::Any.new("Alice"), "age" => JSON::Any.new(30_i64)}
errors = Jargon::Validator.validate(data, schema)
# => [] (empty = valid)
bad_data = {"name" => JSON::Any.new(""), "extra" => JSON::Any.new("?")}
errors = Jargon::Validator.validate(bad_data, schema)
# => ["Value for name must be at least 1 characters",
# "Unknown property 'extra': additionalProperties is false"]
The validator supports all the same constraints as CLI parsing: types, required fields, enums, numeric ranges, string patterns, formats, array constraints, const, $ref, nested objects, and additionalProperties.
API
# Create CLI (program name first, named schema parameter)
cli = Jargon.cli(program_name, json: json_string)
cli = Jargon.cli(program_name, yaml: yaml_string)
cli = Jargon.cli(program_name, file: "schema.json")
# For subcommands (no root schema)
cli = Jargon.new(program_name)
cli.subcommand("name", json: schema_string)
cli.subcommand("name", yaml: schema_string)
cli.subcommand("name", file: "schema.yaml") # single-doc file
cli.subcommand(file: "commands.yaml") # multi-doc as top-level
cli.subcommand("parent", file: "commands.yaml") # multi-doc as nested
# Merge global options into subcommand schema
merged = Jargon.merge(subcommand_schema, global_schema)
# Run with automatic help/completions/error handling (recommended)
cli.run { |result| puts result.to_pretty_json }
cli.run(ARGV) { |result| ... }
result = cli.run # without block, returns Result
# Parse arguments - returns Result with errors array
result = cli.parse(ARGV)
result = cli.parse(ARGV, defaults: config)
# Get data as JSON - returns JSON::Any, raises ParseError on errors
data = cli.json(ARGV)
data = cli.json(ARGV, defaults: config)
# Config file loading
config = cli.load_config # merge all found configs (project wins)
config = cli.load_config(merge: false) # first found wins
paths = cli.config_paths # list of paths searched
# Result methods (from parse or run)
result.valid? # => true/false
result.errors # => Array(String)
result.data # => JSON::Any
result.to_json # => compact JSON string
result.to_pretty_json # => formatted JSON string
result["key"] # => access values
result.subcommand # => String? (nil if no subcommands)
# Help/completion detection (when using parse)
result.help_requested? # => true if --help/-h was passed
result.help_subcommand # => String? (which subcommand's help, nil for top-level)
result.completion_requested? # => true if --completions was passed
result.completion_shell # => String? ("bash", "zsh", or "fish")
# Help text
cli.help # => usage string with all options
cli.help("fetch") # => help for specific subcommand
cli.help("config set") # => help for nested subcommand
# Completion shims (pass command: to target a separate helper binary)
cli.bash_completion # => bash completion shim
cli.zsh_completion # => zsh completion shim
cli.fish_completion # => fish completion shim
cli.bash_completion(command: "app-complete")
# Dynamic completers
cli.completer("forget.query") { |ctx| ... } # register; returns Array(String)
cli.handle_completion # answer __complete + exit (no-op otherwise)
# Standalone validation (no CLI needed)
errors = Jargon::Validator.validate(data_hash, schema) # => Array(String)
# Schema introspection (for GUI/tooling consumers)
schema = cli.schema # => Jargon::Schema? (root schema, if any)
schema = cli.subcommands["name"] # => Jargon::Schema | Jargon::CLI
schema.root # => Jargon::Property (command-level)
schema.root.properties # => Hash(String, Property)?
schema.positional # => Array(String)
property.extensions # => Hash(String, JSON::Any) (x-* annotations, keys as written)
property.extensions["x-ui"]? # => JSON::Any?
Development
Prerequisites
- Crystal >= 1.18.2
Running Tests
shards install
crystal spec
Project Structure
src/
├── jargon.cr # Main module, convenience methods
└── jargon/
├── cli.cr # Core CLI parser
├── schema.cr # JSON Schema parsing
├── schema/property.cr # Property definitions
├── result.cr # Parse result container
├── validator.cr # Standalone schema validator
├── config.cr # Config file loading (XDG)
├── help.cr # Help text generation
└── completion.cr # Shell completion scripts
spec/
└── jargon_spec.cr # Test suite
Building Docs
crystal docs
open docs/index.html
License
MIT