httpcliparser parses HTTP CLI input strings into request headers, query params,
and JSON body data.
It is extracted from get so the parsing logic
can be reused independently in CLIs, test helpers, and other request-building
tools.
cargo add httpcliparseruse httpcliparser::{parse_input, ParsedHeader, ParsedInput, ParsedQueryParam};
use serde_json::json;
let parsed = parse_input([
"Authorization:Bearer token",
"q==hello world",
"foo[bar]=baz",
"is_draft:=true",
])?;
assert_eq!(
parsed,
ParsedInput {
headers: vec![ParsedHeader {
name: "Authorization".into(),
value: "Bearer token".into(),
}],
query_params: vec![ParsedQueryParam {
name: "q".into(),
value: "hello world".into(),
}],
body: Some(json!({
"foo": { "bar": "baz" },
"is_draft": true,
})),
}
);
# Ok::<(), httpcliparser::ParseInputError>(())Each input part is parsed independently using this precedence order:
path:=jsonname==valueHeader:Valuepath=value
If no body assignments are present, ParsedInput.body is None.
| Form | Meaning | Example |
|---|---|---|
path:=json |
Body assignment with typed JSON parsing | count:=2 |
name==value |
Query parameter | page==2 |
Header:Value |
Header | Accept:application/json |
path=value |
Body assignment with a raw string value | title=hello |
Header:Value
- Header names allow ASCII letters, numbers,
-, and_. - Header values may contain additional
:characters. - Values may contain spaces.
- Repeated headers are preserved in input order.
Examples:
Accept:application/json
X-Trace-Id:abc-123
Authorization:Bearer token
name==value
- Query names can contain any character except
=. - Query names must not be empty.
- Empty values are allowed.
- Names and values may contain spaces.
- Repeated keys are preserved in order.
Examples:
q==rust
page==2
foo bar==baz qux
tag==
tag==parser
path=value stores a JSON string.
path:=json parses the right-hand side as JSON, so booleans, numbers, arrays,
objects, and null are preserved.
Examples:
title=hello
enabled:=true
count:=2
labels:=["rust","cli"]
meta:={"owner":"jonathan"}
foo:="string"
foo:=null
String assignments always stay strings:
enabled=true
count=2
becomes:
{
"enabled": "true",
"count": "2"
}Body paths are built from segments. The parser supports all of these segment forms:
| Segment form | Meaning | Example |
|---|---|---|
foo |
Bare object key | user=name |
.foo |
Dotted object key | user.name=alex |
[foo] |
Bracket object key | user[name]=alex |
0 |
Bare array index | 0=first |
.0 |
Dotted array index | items.0=first |
[0] |
Bracket array index | items[0]=first |
[] |
Append to array | items[]=next |
Important path rules:
- Bare and dotted numeric segments are treated as array indexes.
- Bracket keys allow characters that plain keys do not, such as
.inmeta[build.version]=1. - Plain object keys stop at
.,[,:, and=. - Bracket keys continue until the next
]. - Segments can be mixed freely across objects and arrays.
- Separators are flexible, so forms like
foo[].bar=baz,foo[]bar=baz, andfoo.3[][]a[]4[].b[c][][d]=xare all valid.
Examples:
project.name=apollo
project[build.version]=v1
items[]=a
items[]=b
items[2]=third
items.0=first
root[0].user[name]=alex
[]=first
[]=second
- Missing objects and arrays are created automatically while traversing a path.
- Sparse array indexes are padded with
null. - Reassigning the same path overwrites the previous value.
- Repeating
[]appends new values in order. nullplaceholders can later become objects or arrays if a deeper path needs them.- If traversal hits an existing non-container value, parsing fails with a type mismatch error.
Examples:
foo[3]=bar
becomes:
{
"foo": [null, null, null, "bar"]
}foo=bar
foo=baz
becomes:
{
"foo": "baz"
}foo:=null
foo[bar]=baz
becomes:
{
"foo": {
"bar": "baz"
}
}Headers, query params, and body assignments can all be mixed in one parse call:
use httpcliparser::{parse_input, ParsedHeader, ParsedInput, ParsedQueryParam};
use serde_json::json;
let parsed = parse_input([
"Accept:application/json",
"expand==owner",
"expand==labels",
"title=write-readme",
"priority:=2",
"meta[tags][]=docs",
])?;
assert_eq!(
parsed,
ParsedInput {
headers: vec![ParsedHeader {
name: "Accept".into(),
value: "application/json".into(),
}],
query_params: vec![
ParsedQueryParam {
name: "expand".into(),
value: "owner".into(),
},
ParsedQueryParam {
name: "expand".into(),
value: "labels".into(),
},
],
body: Some(json!({
"title": "write-readme",
"priority": 2,
"meta": {
"tags": ["docs"]
}
})),
}
);
# Ok::<(), httpcliparser::ParseInputError>(())The parser reports three kinds of failures:
unexpected input: ...for invalid headers, query params, or path syntax.invalid JSON value in ...when a:=assignment is not valid JSON.- Type mismatch errors when a later path tries to traverse through a scalar value as if it were an object or array.
This repo includes a GitHub Actions publish workflow.
To publish a release:
- Configure trusted publishing for this repository on crates.io.
- Bump
versioninCargo.toml. - Push a matching tag like
v0.1.0.
The publish workflow reruns formatting, linting, tests, verifies the tag matches the crate version, packages the crate, exchanges the GitHub Actions OIDC token for a short-lived crates.io token, and then publishes the crate.