Parser Combinators

id_effect_parse brings small, composable parsers to the workspace — the same functional pattern as schema combinators in Part IV, but oriented toward text and byte streams rather than JSON Unknown values.

The Parser type

A Parser<I, O, E> wraps a function I -> Result<(O, I), E>: parsed output plus remaining input.

#![allow(unused)]
fn main() {
use id_effect_parse::{char, int, parse_str, Parser};

let number = char('(')
    .and_then(|_| int())
    .and_then(|n| char(')').map(move |_| n));

let (value, rest) = parse_str(&number, "(42) rest").unwrap();
assert_eq!(value, 42);
assert_eq!(rest, " rest");
}

Core combinators:

CombinatorRole
maptransform parsed output
and_thensequence dependent parsers
alttry another parser on failure
manyrepeat until the inner parser fails

Built-ins such as char, tag, int, and ws cover common text needs.

Pretty printing

The Pretty trait builds Doc values — a Wadler-style document tree rendered with a line width budget:

#![allow(unused)]
fn main() {
use id_effect_parse::{Doc, Pretty};

let doc = Doc::text("users")
    .cat(Doc::line())
    .cat(["alice", "bob"].pretty())
    .group();

println!("{}", doc.render(40));
}

Use pretty printers for debug output, REPLs, and human-readable config — not for wire formats (use Codec instead).

Invertible codecs

Codec pairs parse and print so formats round-trip:

#![allow(unused)]
fn main() {
use id_effect_parse::codec::quoted_string;

let codec = quoted_string();
let wire = codec.print(&"hello".to_string());
let (parsed, _) = codec.parse(wire).unwrap();
assert_eq!(parsed, "hello");
}

When you need lossless serialization with a parser-shaped API, start with Codec::new.

Diffs

Diff<T> describes value-level changes (Unchanged, Added, Removed, Changed). Helpers like diff_values and diff_option support config drift and snapshot tests.

Parsing Stream chunks

Collect stream chunks, flatten, then parse — parse_stream for typed buffers, parse_text_stream for UTF-8 text:

#![allow(unused)]
fn main() {
use id_effect::{Chunk, Stream, run_blocking};
use id_effect_parse::{parse_text_stream, tag};

let parser = tag("ping");
let stream = Stream::from_iterable(vec![Chunk::from_vec(b"ping".to_vec())]);
let value = run_blocking(parse_text_stream(&parser, stream), ()).unwrap();
assert_eq!(value, "ping");
}

Schema bridge

SchemaBridge connects Schema values to text parsers:

  • parser_for_json — parse JSON text, then decode_unknown
  • parser_for_string_wire — when the wire type is String
  • parser_for::<T>() — for types implementing HasSchema

Boundary validation for external data still belongs to id_effect::schema (Part IV). Use schema at the edge; use SchemaBridge when a text protocol should reuse the same schema.

#![allow(unused)]
fn main() {
use id_effect::schema::{HasSchema, i64, string, struct_};
use id_effect_parse::SchemaBridge;

let schema = struct_("name", string(), "age", i64());
let parser = SchemaBridge::parser_for_json(schema);
let (person, _) = parser.parse(r#"{"name":"Ada","age":36}"#.to_string()).unwrap();
assert_eq!(person, ("Ada".to_string(), 36));
}

#[derive(SchemaParser)]

The proc macro generates schema(), parser(), and HasSchema for structs with named fields (see Part V chapter 24).

Next steps

  • Part IV Schema for JSON and API boundaries
  • Part IV Streams for chunk backpressure and collection
  • Workspace crate: crates/id_effect_parse/