From d3825df8adaf2accda89890026c4ca5bb45c2c39 Mon Sep 17 00:00:00 2001
From: Eric Rodrigues Pires
Date: Sat, 29 Nov 2025 22:35:27 -0300
Subject: [PATCH] Documentation for duperq and duperfmt
---
Cargo.lock | 1 -
README.md | 3 +-
axum_duper/src/lib.rs | 2 -
duper/CHANGELOG.md | 6 +
duper/src/ast.rs | 2 +-
duper_website/docs/.vitepress/config.mts | 7 +
duper_website/docs/duperfmt.md | 17 +++
duper_website/docs/duperq.md | 165 +++++++++++++++++++++++
duperfmt/src/lib.rs | 6 +
duperq/Cargo.toml | 1 -
duperq/README.md | 2 +-
duperq/src/accessor.rs | 25 +++-
duperq/src/filter.rs | 1 +
duperq/src/formatter.rs | 20 ++-
duperq/src/lib.rs | 5 +
duperq/src/main.rs | 144 +++++++-------------
duperq/src/processor.rs | 5 +
duperq/src/query.rs | 83 +++++++-----
serde_duper/src/lib.rs | 4 +-
tree-sitter-duper/bindings/rust/lib.rs | 1 +
tree-sitter-duper/src/scanner.c | 12 +-
21 files changed, 364 insertions(+), 148 deletions(-)
create mode 100644 duper_website/docs/duperfmt.md
create mode 100644 duper_website/docs/duperq.md
diff --git a/Cargo.lock b/Cargo.lock
index 9bd490e..050b4ff 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1029,7 +1029,6 @@ dependencies = [
"clap",
"duper",
"futures",
- "glob",
"num_cpus",
"regex",
"smol",
diff --git a/README.md b/README.md
index e531e20..34497e2 100644
--- a/README.md
+++ b/README.md
@@ -59,4 +59,5 @@ See [the specification](https://duper.dev.br/spec.html) or the [EBNF grammar](ht
- [`duperfmt`](./duperfmt/): Duper formatter based on Topiary.
- [`duper_lsp`](./duper_lsp/): Duper LSP.
- [`duper-vs-code`](./duper-vs-code/): Duper extension for Visual Studio Code.
-- [`duper_website`](./duper_website/): Official website for Duper, including the specification and WebAssembly-based playground.
\ No newline at end of file
+- [`duperq`](./duperq): A fast Duper and JSON filter/processor.
+- [`duper_website`](./duper_website/): Official website for Duper, including the specification and WebAssembly-based playground.
diff --git a/axum_duper/src/lib.rs b/axum_duper/src/lib.rs
index dc475f9..e866c87 100644
--- a/axum_duper/src/lib.rs
+++ b/axum_duper/src/lib.rs
@@ -5,8 +5,6 @@
//!
//! This crate provides the [`Duper`] struct, which can be used to extract typed
//! information from request's body, or to serialize a structured response.
-//!
-//! Under the hood, it wraps [`serde_duper`].
use std::ops::Deref;
diff --git a/duper/CHANGELOG.md b/duper/CHANGELOG.md
index 5e09b63..61cfdef 100644
--- a/duper/CHANGELOG.md
+++ b/duper/CHANGELOG.md
@@ -1,5 +1,11 @@
# Changelog
+## Unreleased
+
+### Changed
+
+- Expose most parser functions to the public API.
+
## 0.4.2 (2025-11-25)
### Fixed
diff --git a/duper/src/ast.rs b/duper/src/ast.rs
index ed56bfa..bbf670b 100644
--- a/duper/src/ast.rs
+++ b/duper/src/ast.rs
@@ -120,7 +120,7 @@ pub enum DuperObjectTryFromError<'a> {
DuplicateKey(Cow<'a, str>),
}
-/// Possible errors generated by [`DuperTemporal::try_from()`].
+/// Possible errors generated by `DuperTemporal::try_*_from()`.
#[derive(Debug, Clone)]
pub enum DuperTemporalTryFromError<'a> {
/// The Temporal string was empty.
diff --git a/duper_website/docs/.vitepress/config.mts b/duper_website/docs/.vitepress/config.mts
index e015af7..83f26aa 100644
--- a/duper_website/docs/.vitepress/config.mts
+++ b/duper_website/docs/.vitepress/config.mts
@@ -69,6 +69,13 @@ export default async () => {
text: "Editor support",
items: [{ text: "Visual Studio Code", link: "/vs-code" }],
},
+ {
+ text: "Tools",
+ items: [
+ { text: "duperq", link: "/duperq" },
+ { text: "duperfmt", link: "/duperfmt" },
+ ],
+ },
{ text: "Specification", link: "/spec" },
],
diff --git a/duper_website/docs/duperfmt.md b/duper_website/docs/duperfmt.md
new file mode 100644
index 0000000..ad87761
--- /dev/null
+++ b/duper_website/docs/duperfmt.md
@@ -0,0 +1,17 @@
+# duperfmt
+
+`duperfmt` is a Duper formatter based on tree-sitter and Topiary. It powers `duper_lsp`'s own formatting engine.
+
+## Installation
+
+```bash
+cargo install --locked duperfmt
+```
+
+## Basic usage
+
+```bash
+duperfmt -f input.duper -o output.duper
+```
+
+Run `duperfmt --help` for more details.
diff --git a/duper_website/docs/duperq.md b/duper_website/docs/duperq.md
new file mode 100644
index 0000000..45c79e0
--- /dev/null
+++ b/duper_website/docs/duperq.md
@@ -0,0 +1,165 @@
+# duperq
+
+`duperq` is a fast filter and processor of Duper files and logs, which also works with JSON.
+
+## Installation
+
+```bash
+cargo install --locked duperq
+```
+
+## Basic usage
+
+As an example, we'll assume the following data from a log format:
+
+```duper
+{
+ traceId: UUID("a2ce2f29-84cf-47c9-a877-381855d59e77"),
+ spanId: UUID("78ee3c78-c090-43f2-8568-f4542dc10ea5"),
+ timestamp: "2025-11-29T22:21:45.133Z",
+ level: "INFO",
+ service: "store-webapp",
+ development: false,
+ http: {
+ method: "GET",
+ url: "/shopping-cart",
+ statusCode: 200,
+ address: ("192.168.1.100", 14567),
+ userAgent: "Mozilla/5.0",
+ duration: Duration('PT0.14567S'),
+ history: ["/", "/search?q=headphones+", "/products/42"],
+ },
+}
+```
+
+You can read files by passing them after the `duperq` filter:
+
+```bash
+duperq "filter ." path/to/**/*.duper
+```
+
+You can also read lines of Duper values from stdin.
+
+```bash
+tail -f path/to/app.log | duperq "filter ."
+```
+
+### Filtering
+
+To filter results, use the `filter` param in the query. You can bypass filtering by passing an empty query to `duperq`.
+
+```bash
+duperq "" log.duper
+```
+
+To access fields in objects or arrays of objects, use `.fieldName`. For complex keys, you can use quotes and Duper escaping, i.e. `."special key"`.
+
+```bash
+duperq "filter .level == \"INFO\"" log.duper
+# ... equivalent to ...
+duperq "filter .\"level\" == \"INFO\"" log.duper
+```
+
+You can concatenate fields to access nested objects:
+
+```bash
+duperq "filter .http.method == \"GET\"" log.duper
+```
+
+You can also combine filters with `and`/`&&` or `or`/`||`. To check if a field simply exists, use `exists(...)`.
+
+```bash
+duperq "filter (.http.url = \"/admin\" || .level = \"INFO\") && exists(.spanId)" log.duper
+```
+
+You can use comparison operators (`==` or `=` for equality; `!=` or `<>` for inequality; `<`, `<=`, `>`, `>=`) as you'd expect. For sized values (objects, arrays, tuples, strings and bytes), you can use the `len(...)` function.
+
+```bash
+duperq "filter .http.statusCode >= 400" log.duper
+duperq "filter len(.http.history) > 2" log.duper
+duperq "filter .http.duration < Duration('PT1S')" log.duper
+```
+
+You can also match strings/bytes with [Rust regexes](https://docs.rs/regex/) via `=~ "regex"`, or access the current element with a sole `.`. To filter elements in an array, add a `[selector operator value]` to the fields. For example, we can filter history values that contain "headphones" by putting all of these together:
+
+```bash
+duperq "filter .http.history[. =~ \"headphones\"]" log.duper
+```
+
+To check if a value is truthy, simply use the selector without an operator. You can also negate the result of a filter with `!`:
+
+```bash
+duperq "filter !.development" log.duper
+```
+
+To validate that a value is of a certain type, use the `is` operator. The valid right-handside operands are:
+
+- `Object`
+- `Array`
+- `Tuple`
+- `String`
+- `Bytes`
+- `Instant`
+- `ZonedDateTime`
+- `PlainDate`
+- `PlainTime`
+- `PlainDateTime`
+- `PlainYearMonth`
+- `PlainMonthDay`
+- `Duration`
+- `Temporal`
+- `Integer`
+- `Float`
+- `Number`
+- `Boolean`
+- `Null`
+
+```bash
+duperq "filter .http.address is Tuple" log.duper
+```
+
+You can index into an array/tuple with `[index]`. Negative indexes also work, but they do not wrap around. To filter over an identifier, use `identifier(...)`.
+
+```bash
+duperq "filter identifier(.http.address[0]) == \"IPv4Address\"" log.duper
+# We can use a regex instead
+duperq "filter identifier(.http.address[0]) =~ \"(?i)^ipv\\\\daddress\$\"" log.duper
+# To check if there is NO identifier
+duperq "filter identifier(.http.userAgent) == null" log.duper
+# To check if there is ANY identifier
+duperq "filter identifier(.traceId) <> null" log.duper
+```
+
+You can use [ranges](https://doc.rust-lang.org/std/ops/struct.Range.html) over array values.
+
+```bash
+duperq "filter .http.history[..2] == \"/\"" log.duper
+```
+
+Last but not least, you can cast values into different types with `cast(..., type)`, with the same possible types from the `is` operator. This can be useful when dealing with JSON data, where there are no tuples or Temporal values, or to treat a tuple as an array. In our example, we can transform the string-only timestamp into a filterable value:
+
+```bash
+duperq "filter cast(.timestamp, Instant) > Instant('2025-11-01T00:00:00-03:00')" log.duper
+```
+
+### Manipulation
+
+Other than filtering data, you may also skip the first values with `skip X`, or limit the number of filtered values you take with `take X`. These operations can be combined with pipes `|`:
+
+```bash
+duperq "filter .development | skip 3 | filter .level = \"ERROR\" | take 10" path/to/**/*.duper
+```
+
+### Output
+
+By default, `duperq` serializes output data into a single-line format. You can change this by piping the output of your query to:
+
+- `| ansi`: Prints with ANSI colors.
+- `| pretty-print`: Pretty-prints values over multiple lines with indentation.
+- `| format`: Allows you to print arbitrary strings, replacing `${...}` blocks with the selector inside. String values will have their quotes stripped. Missing values will be printed as ``.
+
+```bash
+duperq "filter . | format \"[\${.level}] \${.http.statusCode} - \${.http.method} \${.http.url}\"" log.duper
+```
+
+Formats must always be the last block in your query workflow.
diff --git a/duperfmt/src/lib.rs b/duperfmt/src/lib.rs
index ee09560..6ef337d 100644
--- a/duperfmt/src/lib.rs
+++ b/duperfmt/src/lib.rs
@@ -1,3 +1,8 @@
+#![doc(html_logo_url = "https://duper.dev.br/logos/duper-100-100.png")]
+//!
+#![doc = include_str!("../../duper_website/docs/duperfmt.md")]
+//!
+
use std::io::Write;
use topiary_core::{Language, Operation, TopiaryQuery, formatter_tree};
@@ -5,6 +10,7 @@ use tree_sitter::Tree;
const DUPER_QUERY: &str = include_str!("./duper.scm");
+/// Given a Duper [`Tree`] built from an input, formats said input into the output buffer.
pub fn format_duper(
tree: Tree,
input: &str,
diff --git a/duperq/Cargo.toml b/duperq/Cargo.toml
index f44eba9..8707354 100644
--- a/duperq/Cargo.toml
+++ b/duperq/Cargo.toml
@@ -17,7 +17,6 @@ chumsky = "0.11.2"
clap = { version = "4.5.53", features = ["derive"] }
duper = { version = "0.4.3", path = "../duper", features = ["ansi"] }
futures = "0.3.31"
-glob = "0.3.3"
num_cpus = "1.17.0"
regex = "1.12.2"
smol = "2.0.2"
diff --git a/duperq/README.md b/duperq/README.md
index 25f0c69..5632561 100644
--- a/duperq/README.md
+++ b/duperq/README.md
@@ -8,6 +8,6 @@
-A high-performance Duper/JSON filter and processor.
+A fast Duper and JSON filter/processor.
[Check out the official website for Duper.](https://duper.dev.br)
diff --git a/duperq/src/accessor.rs b/duperq/src/accessor.rs
index 6d5de31..aab59d9 100644
--- a/duperq/src/accessor.rs
+++ b/duperq/src/accessor.rs
@@ -31,6 +31,17 @@ impl DuperAccessor for FlattenedAccessor {
// Base accessors
+pub(crate) struct SelfAccessor;
+
+impl DuperAccessor for SelfAccessor {
+ fn access<'accessor: 'value, 'value>(
+ &'accessor self,
+ value: &'value DuperValue<'value>,
+ ) -> AccessorReturn<'value> {
+ Box::new(iter::once(value))
+ }
+}
+
pub(crate) struct FieldAccessor(pub(crate) String);
impl DuperAccessor for FieldAccessor {
@@ -72,6 +83,8 @@ impl DuperAccessor for IndexAccessor {
) -> AccessorReturn<'value> {
if let DuperInner::Array(array) = &value.inner {
Box::new(array.get(self.0).into_iter())
+ } else if let DuperInner::Tuple(tuple) = &value.inner {
+ Box::new(tuple.get(self.0).into_iter())
} else {
Box::new(iter::empty())
}
@@ -86,7 +99,17 @@ impl DuperAccessor for ReverseIndexAccessor {
value: &'value DuperValue<'value>,
) -> AccessorReturn<'value> {
if let DuperInner::Array(array) = &value.inner {
- Box::new(array.get(array.len() - self.0).into_iter())
+ if let Some(index) = array.len().checked_sub(self.0) {
+ Box::new(array.get(index).into_iter())
+ } else {
+ Box::new(iter::empty())
+ }
+ } else if let DuperInner::Tuple(tuple) = &value.inner {
+ if let Some(index) = tuple.len().checked_sub(self.0) {
+ Box::new(tuple.get(index).into_iter())
+ } else {
+ Box::new(iter::empty())
+ }
} else {
Box::new(iter::empty())
}
diff --git a/duperq/src/filter.rs b/duperq/src/filter.rs
index 2b32dbf..0dae53a 100644
--- a/duperq/src/filter.rs
+++ b/duperq/src/filter.rs
@@ -226,6 +226,7 @@ impl DuperFilter for EqFilter {
},
(EqValue::Len(this), DuperInner::Object(that)) => *this == that.len(),
(EqValue::Len(this), DuperInner::Array(that)) => *this == that.len(),
+ (EqValue::Len(this), DuperInner::Tuple(that)) => *this == that.len(),
(EqValue::Len(this), DuperInner::String(that)) => *this == that.as_ref().len(),
(EqValue::Len(this), DuperInner::Bytes(that)) => *this == that.as_ref().len(),
(EqValue::Tuple(this), DuperInner::Tuple(that)) => {
diff --git a/duperq/src/formatter.rs b/duperq/src/formatter.rs
index 3561b94..c0f6118 100644
--- a/duperq/src/formatter.rs
+++ b/duperq/src/formatter.rs
@@ -10,11 +10,11 @@ use duper::{
visitor::DuperVisitor,
};
-use crate::accessor::DuperAccessor;
+use crate::{accessor::DuperAccessor, types::DuperType};
pub(crate) enum FormatterAtom {
Fixed(String),
- Dynamic(Box),
+ Dynamic(Box, Option),
}
pub(crate) struct Formatter {
@@ -41,10 +41,20 @@ impl Formatter {
for atom in &self.atoms {
match atom {
FormatterAtom::Fixed(fixed) => buf.push_str(&fixed),
- FormatterAtom::Dynamic(duper_accessor) => {
+ FormatterAtom::Dynamic(duper_accessor, typ) => {
match duper_accessor.access(&value).into_iter().next() {
- Some(value) => buf.push_str(&self.visitor.visit(value)),
- None => buf.push_str("-MISSING-"),
+ Some(value) => {
+ if let Some(typ) = typ {
+ if let Some(value) = typ.cast(value) {
+ buf.push_str(&self.visitor.visit(&value))
+ } else {
+ buf.push_str("")
+ }
+ } else {
+ buf.push_str(&self.visitor.visit(value))
+ }
+ }
+ None => buf.push_str(""),
}
}
}
diff --git a/duperq/src/lib.rs b/duperq/src/lib.rs
index 96bb0c4..c3c0505 100644
--- a/duperq/src/lib.rs
+++ b/duperq/src/lib.rs
@@ -1,3 +1,8 @@
+#![doc(html_logo_url = "https://duper.dev.br/logos/duper-100-100.png")]
+//!
+#![doc = include_str!("../../duper_website/docs/duperq.md")]
+//!
+
mod accessor;
mod filter;
mod formatter;
diff --git a/duperq/src/main.rs b/duperq/src/main.rs
index c1954c5..ae449f9 100644
--- a/duperq/src/main.rs
+++ b/duperq/src/main.rs
@@ -1,10 +1,9 @@
-use std::{fmt::Display, path::PathBuf};
+use std::path::PathBuf;
use chumsky::Parser as _;
use clap::Parser;
use duper::DuperParser;
use duperq::query;
-use glob::glob;
use smol::{
LocalExecutor, Unblock,
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
@@ -14,41 +13,16 @@ use smol::{
#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
- /// Query to run.
- query: String,
-
- /// Glob of files to read from. If missing, defaults to stdin.
- glob: Option,
-
/// If set, disables logs about parsing errors from being printed to stderr.
#[arg(short = 'E', long)]
disable_stderr: bool,
-}
-
-enum FileReadError {
- Glob(glob::GlobError),
- Io(std::io::Error),
-}
-
-impl From for FileReadError {
- fn from(value: glob::GlobError) -> Self {
- Self::Glob(value)
- }
-}
-impl From for FileReadError {
- fn from(value: std::io::Error) -> Self {
- Self::Io(value)
- }
-}
+ /// Query to run.
+ query: String,
-impl Display for FileReadError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- FileReadError::Glob(error) => error.fmt(f),
- FileReadError::Io(error) => error.fmt(f),
- }
- }
+ /// Files to read from. If missing, defaults to stdin.
+ #[arg(name = "FILE")]
+ files: Vec,
}
fn main() -> anyhow::Result<()> {
@@ -75,66 +49,61 @@ fn main() -> anyhow::Result<()> {
while let Ok(value) = receiver.recv().await {
output.process(value).await;
}
+ output.close().await;
}));
(pipeline_fn)(sender)
});
- let glob = if let Some(duper_glob) = cli.glob {
- Some(glob(&duper_glob)?)
- } else {
- None
- };
+ let files = cli.files;
- if let Some(glob) = glob {
- let (pathbuf_sender, pathbuf_receiver) =
- smol::channel::bounded::>(128);
- let (file_sender, file_receiver) =
- smol::channel::bounded::>(128);
- // Iterate over glob
+ if files.is_empty() {
+ // Read from stdin
tasks.push(executor.spawn(async move {
- for entry in glob {
- match entry {
- Err(_) if cli.disable_stderr => continue,
- Ok(_) | Err(_) => {
- if pathbuf_sender
- .send(entry.map_err(|error| error.into()))
- .await
- .is_err()
- {
- break;
+ let stdin = BufReader::new(Unblock::new(std::io::stdin()));
+ let mut lines = stdin.lines();
+ while let Some(Ok(line)) = lines.next().await {
+ match DuperParser::parse_duper_trunk(&line) {
+ Ok(trunk) => sink.process(trunk.static_clone()).await,
+ Err(errors) => {
+ if !cli.disable_stderr {
+ if let Ok(parse_error) =
+ DuperParser::prettify_error(&line, &errors, None)
+ {
+ let _ = stderr.write_all(parse_error.as_bytes()).await;
+ }
}
}
}
}
+ sink.close().await;
+ }));
+ } else {
+ let (pathbuf_sender, pathbuf_receiver) = smol::channel::bounded::(128);
+ let (file_sender, file_receiver) =
+ smol::channel::bounded::>(128);
+ // Iterate over files
+ tasks.push(executor.spawn(async move {
+ for entry in files {
+ if pathbuf_sender.send(entry).await.is_err() {
+ break;
+ }
+ }
}));
// Read files
tasks.extend((0..num_cpus::get()).map(|_| {
let file_sender = file_sender.clone();
let pathbuf_receiver = pathbuf_receiver.clone();
executor.spawn(async move {
- while let Ok(msg) = pathbuf_receiver.recv().await {
- match msg {
- Ok(pathbuf) => {
- let string = smol::fs::read_to_string(&pathbuf).await;
- match string {
- Err(_) if cli.disable_stderr => continue,
- Ok(_) | Err(_) => {
- if file_sender
- .send(
- string
- .map(move |string| (pathbuf, string))
- .map_err(|error| error.into()),
- )
- .await
- .is_err()
- {
- break;
- }
- }
- }
- }
- Err(error) => {
- if file_sender.send(Err(error)).await.is_err() {
+ while let Ok(pathbuf) = pathbuf_receiver.recv().await {
+ let string = smol::fs::read_to_string(&pathbuf).await;
+ match string {
+ Err(_) if cli.disable_stderr => continue,
+ Ok(_) | Err(_) => {
+ if file_sender
+ .send(string.map(move |string| (pathbuf, string)))
+ .await
+ .is_err()
+ {
break;
}
}
@@ -156,35 +125,18 @@ fn main() -> anyhow::Result<()> {
Some(pathbuf.to_string_lossy().as_ref()),
) {
let _ = stderr.write_all(parse_error.as_bytes()).await;
+ let _ = stderr.flush().await;
}
}
}
},
Err(error) => {
let _ = stderr.write_all(error.to_string().as_bytes()).await;
+ let _ = stderr.flush().await;
}
}
}
- }));
- } else {
- // Read from stdin
- tasks.push(executor.spawn(async move {
- let stdin = BufReader::new(Unblock::new(std::io::stdin()));
- let mut lines = stdin.lines();
- while let Some(Ok(line)) = lines.next().await {
- match DuperParser::parse_duper_trunk(&line) {
- Ok(trunk) => sink.process(trunk.static_clone()).await,
- Err(errors) => {
- if !cli.disable_stderr {
- if let Ok(parse_error) =
- DuperParser::prettify_error(&line, &errors, None)
- {
- let _ = stderr.write_all(parse_error.as_bytes()).await;
- }
- }
- }
- }
- }
+ sink.close().await;
}));
}
diff --git a/duperq/src/processor.rs b/duperq/src/processor.rs
index 020c0b9..02b0f2c 100644
--- a/duperq/src/processor.rs
+++ b/duperq/src/processor.rs
@@ -123,4 +123,9 @@ impl Processor for OutputProcessor {
.await
.expect("stdout was closed");
}
+
+ async fn close(&mut self) {
+ self.stdout.flush().await.expect("stdout was closed");
+ self.stdout.close().await.expect("stdout was closed");
+ }
}
diff --git a/duperq/src/query.rs b/duperq/src/query.rs
index 3cae284..410668a 100644
--- a/duperq/src/query.rs
+++ b/duperq/src/query.rs
@@ -2,14 +2,14 @@ use chumsky::prelude::*;
use duper::{
Ansi, DuperInner, DuperValue, PrettyPrinter, Serializer,
escape::unescape_str,
- parser::{identified_value, identifier, integer, object_key},
+ parser::{identified_value, integer, object_key, quoted_string},
};
use smol::channel;
use crate::{
accessor::{
AnyAccessor, DuperAccessor, FieldAccessor, FilterAccessor, FlattenedAccessor,
- IndexAccessor, RangeIndexAccessor, ReverseIndexAccessor,
+ IndexAccessor, RangeIndexAccessor, ReverseIndexAccessor, SelfAccessor,
},
filter::{
AccessorFilter, AndFilter, CastFilter, CmpValue, DuperFilter, EqFilter, EqValue, GeFilter,
@@ -24,9 +24,27 @@ use crate::{
pub(crate) type CreateProcessorFn =
Box>) -> Box>;
+/// Parses a `duperq` query.
pub fn query<'a>()
-> impl Parser<'a, &'a str, (Vec, Box), extra::Err>>
{
+ let output = choice((
+ just("format").padded().ignore_then(fmt().padded()),
+ just("ansi").padded().map(|_| {
+ let mut ansi = Ansi::default();
+ OutputProcessor::new(Box::new(move |value| {
+ ansi.to_ansi(value).unwrap_or_default()
+ }))
+ }),
+ just("pretty-print").padded().map(|_| {
+ let mut pretty_printer = PrettyPrinter::default();
+ OutputProcessor::new(Box::new(move |value| {
+ pretty_printer.pretty_print(value).into_bytes()
+ }))
+ }),
+ ))
+ .padded();
+
choice((
just("filter").padded().ignore_then(filter()).map(|filter| {
Box::new(move |sender| {
@@ -62,28 +80,14 @@ pub fn query<'a>()
}),
))
.separated_by(just('|'))
- .collect()
+ .collect::>()
.then(
just('|')
.padded()
- .ignore_then(choice((
- just("format").padded().ignore_then(fmt().padded()),
- just("ansi").padded().map(|_| {
- let mut ansi = Ansi::default();
- OutputProcessor::new(Box::new(move |value| {
- ansi.to_ansi(value).unwrap_or_default()
- }))
- }),
- just("pretty-print").padded().map(|_| {
- let mut pretty_printer = PrettyPrinter::default();
- OutputProcessor::new(Box::new(move |value| {
- pretty_printer.pretty_print(value).into_bytes()
- }))
- }),
- )))
+ .ignore_then(output.padded())
.or_not()
- .map(|processor| {
- Box::new(processor.unwrap_or_else(|| {
+ .map(|output| {
+ Box::new(output.unwrap_or_else(|| {
let mut serializer = Serializer::default();
OutputProcessor::new(Box::new(move |value| {
serializer.serialize(value).into_bytes()
@@ -91,6 +95,7 @@ pub fn query<'a>()
})) as Box
}),
)
+ .then_ignore(end())
}
fn filter<'a>() -> impl Parser<'a, &'a str, Box, extra::Err>> + Clone
@@ -139,10 +144,11 @@ fn filter<'a>() -> impl Parser<'a, &'a str, Box, extra::Err()
-> impl Parser<'a, &'a str, Box, extra::Err>> + Clone {
recursive(|accessor| {
- let access = just('.').ignore_then(choice((
- object_key().map(|key: duper::DuperKey<'a>| {
+ let access = choice((
+ just('.').ignore_then(object_key().map(|key: duper::DuperKey<'a>| {
Box::new(FieldAccessor(key.as_ref().into())) as Box
- }),
+ })),
+ just('.').map(|_| Box::new(SelfAccessor) as Box),
integer()
.or_not()
.padded()
@@ -201,7 +207,7 @@ fn accessor<'a>()
text::whitespace()
.delimited_by(just('['), just(']'))
.map(|_| Box::new(AnyAccessor) as Box),
- )));
+ ));
access
.clone()
@@ -223,6 +229,7 @@ fn leaf_filter<'a>(
.ignore_then(
accessor
.clone()
+ .padded()
.then_ignore(just(','))
.then(duper_type().padded())
.map(|(accessor, typ)| {
@@ -345,8 +352,8 @@ fn leaf_filter<'a>(
eq_op
.clone()
.ignore_then(
- identifier()
- .map(|identifier| Some(identifier.to_string()))
+ quoted_string()
+ .map(|identifier| Some(identifier.into_owned()))
.or(just("null").to(None))
.padded(),
)
@@ -356,8 +363,8 @@ fn leaf_filter<'a>(
ne_op
.clone()
.ignore_then(
- identifier()
- .map(|identifier| Some(identifier.to_string()))
+ quoted_string()
+ .map(|identifier| Some(identifier.into_owned()))
.or(just("null").to(None))
.padded(),
)
@@ -473,10 +480,24 @@ fn duper_type<'a>() -> impl Parser<'a, &'a str, DuperType, extra::Err() -> impl Parser<'a, &'a str, OutputProcessor, extra::Err>> {
+fn fmt<'a>() -> impl Parser<'a, &'a str, OutputProcessor, extra::Err>> + Clone {
just('$')
- .ignore_then(accessor().padded().delimited_by(just('{'), just('}')))
- .map(|accessor| FormatterAtom::Dynamic(accessor))
+ .ignore_then(
+ just("cast")
+ .padded()
+ .ignore_then(
+ accessor()
+ .padded()
+ .then_ignore(just(','))
+ .then(duper_type().padded())
+ .map(|(accessor, typ)| FormatterAtom::Dynamic(accessor, Some(typ)))
+ .delimited_by(just('('), just(')')),
+ )
+ .or(accessor()
+ .map(|accessor| FormatterAtom::Dynamic(accessor, None))
+ .padded())
+ .delimited_by(just('{'), just('}')),
+ )
.or(
quoted_inner().try_map(|slice: &str, span| match unescape_str(slice) {
Ok(unescaped) => Ok(FormatterAtom::Fixed(unescaped.clone().into_owned())),
diff --git a/serde_duper/src/lib.rs b/serde_duper/src/lib.rs
index d72eb50..059b09f 100644
--- a/serde_duper/src/lib.rs
+++ b/serde_duper/src/lib.rs
@@ -402,7 +402,7 @@ pub use serde_duper_macros::duper;
/// This conversion can fail if the structure of the input does not match the
/// structure expected by `T`, for example if `T` is a struct type but the input
/// contains something other than a Duper object. It can also fail if the
-/// structure is correct but `T`'s implementation of [`Deserialize`] decides that
+/// structure is correct but `T`'s implementation of [`serde_core::Deserialize`] decides that
/// something is wrong with the data, for example required struct fields are
/// missing from the Duper object or some number is too big to fit in the
/// expected primitive type.
@@ -444,7 +444,7 @@ where
/// This conversion can fail if the structure of the input does not match the
/// structure expected by `T`, for example if `T` is a struct type but the input
/// contains something other than a Duper object. It can also fail if the
-/// structure is correct but `T`'s implementation of [`Deserialize`] decides that
+/// structure is correct but `T`'s implementation of [`serde_core::Deserialize`] decides that
/// something is wrong with the data, for example required struct fields are
/// missing from the Duper object or some number is too big to fit in the
/// expected primitive type.
diff --git a/tree-sitter-duper/bindings/rust/lib.rs b/tree-sitter-duper/bindings/rust/lib.rs
index d71f9f0..da9e11b 100644
--- a/tree-sitter-duper/bindings/rust/lib.rs
+++ b/tree-sitter-duper/bindings/rust/lib.rs
@@ -1,3 +1,4 @@
+#![doc(html_logo_url = "https://duper.dev.br/logos/duper-100-100.png")]
//! This crate provides Duper language support for the [tree-sitter] parsing library.
//!
//! Typically, you will use the [`LANGUAGE`] constant to add this language to a
diff --git a/tree-sitter-duper/src/scanner.c b/tree-sitter-duper/src/scanner.c
index baf5e2a..631b803 100644
--- a/tree-sitter-duper/src/scanner.c
+++ b/tree-sitter-duper/src/scanner.c
@@ -108,8 +108,8 @@ bool tree_sitter_duper_external_scanner_scan(void *payload, TSLexer *lexer,
for (int i = 0; i < 2; i++) {
lexer->advance(lexer, false);
if (lexer->lookahead < '0' ||
- lexer->lookahead > '9' && lexer->lookahead < 'A' ||
- lexer->lookahead > 'F' && lexer->lookahead < 'a' ||
+ (lexer->lookahead > '9' && lexer->lookahead < 'A') ||
+ (lexer->lookahead > 'F' && lexer->lookahead < 'a') ||
lexer->lookahead > 'f') {
return false;
}
@@ -118,8 +118,8 @@ bool tree_sitter_duper_external_scanner_scan(void *payload, TSLexer *lexer,
for (int i = 0; i < 4; i++) {
lexer->advance(lexer, false);
if (lexer->lookahead < '0' ||
- lexer->lookahead > '9' && lexer->lookahead < 'A' ||
- lexer->lookahead > 'F' && lexer->lookahead < 'a' ||
+ (lexer->lookahead > '9' && lexer->lookahead < 'A') ||
+ (lexer->lookahead > 'F' && lexer->lookahead < 'a') ||
lexer->lookahead > 'f') {
return false;
}
@@ -128,8 +128,8 @@ bool tree_sitter_duper_external_scanner_scan(void *payload, TSLexer *lexer,
for (int i = 0; i < 8; i++) {
lexer->advance(lexer, false);
if (lexer->lookahead < '0' ||
- lexer->lookahead > '9' && lexer->lookahead < 'A' ||
- lexer->lookahead > 'F' && lexer->lookahead < 'a' ||
+ (lexer->lookahead > '9' && lexer->lookahead < 'A') ||
+ (lexer->lookahead > 'F' && lexer->lookahead < 'a') ||
lexer->lookahead > 'f') {
return false;
}
--
2.51.2