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 @@ GitHub license

-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