Skip to content

rust: #[rulekit(skip)] on derive(Input) fields - #39

Open
kamaln7 wants to merge 106 commits into
v2from
rust-input-skip
Open

kamaln7 wants to merge 106 commits into
v2from
rust-input-skip

Conversation

@kamaln7

@kamaln7 kamaln7 commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator

Adds #[rulekit(skip)] to the Rust #[derive(rulekit::Input)]. A skipped field is invisible to rules: a rule that reads it gets a missing field, as if the field did not exist.

#[derive(rulekit::Input)]
struct Request {
    host: String,
    #[rulekit(skip)]
    db: DbHandle, // no rulekit conversion; without skip this fails to compile
    #[rulekit(skip)]
    api_key: String, // hidden from rules on purpose
}

Without it, every field must have a type rulekit can read, and every field is readable by rules. Large fields do not need it: input is lazy, so fields a rule does not mention are never read.

Decision needed: this sets a precedent. Go would get the matching struct tag rulekit:"-" if struct input lands (#37).

This reverts 02f8180, which removed the feature from v2 so it can be decided separately.

Checks (local, toolchain 1.97.1): cargo fmt --all --check, cargo clippy --workspace --all-targets --locked -- -D warnings, cargo test --workspace --locked pass. derive_input covers a skipped field reported as missing; the input_skip compile-fail case (skip as an unknown attribute) is removed.

kamaln7 added 30 commits October 5, 2026 15:41
Evaluating several rules against one FromKV input from multiple goroutines
raced on the unsynchronized memo map (fatal: concurrent map read and map
write). Guard the map with a mutex and resolve each lazy path once via a
per-path entry; plain values return before touching the memo.
Add json.Marshaler on *AST plus AST.JSON and AST.JSONSpan, with public
JSONAST/JSONNode/JSONToken/JSONSpan types matching the shape the demo
already emitted. ASTKind and Operator gain String methods.

The demo now uses the library types and is its own module, so it is no
longer part of the root module zip.
Add testdata/vectors (eval, literals, paths, fields, regex, functions,
macros, parse_errors, print, trace, json_input, ast_json) with a schema
README, and vectors_test.go, which strictly decodes and runs every vector
file. Remove the Go tests the vectors now cover; keep Go-only coverage
(AST/token/rewrite APIs, custom functions, Opts.Validate, input adapters,
typed-slice and non-UTF-8 inputs, error wording, fuzzing, benchmarks).
kamaln7 added 29 commits October 5, 2026 22:16
Completes the shared URL rules (R1, R2); all vectors pass.
rewrite(ast, &[Edit { target: NodeId, replacement: &Ast }], mode) ports
Go Rewrite: unchanged source is kept, replacements are printed in the
mode, and overlapping edits are an error. EvalResult's accessors are
renamed to value() and error() to match the Go Result fields.
Every public item is documented (missing_docs is on), with doc examples
for parse/eval, inputs, decode_json, functions and macros, tracing,
printing, and rewrite. Also adds the hidden hooks the benchmarks use
(__bench::cmp_number, the rulekit_size_probe cfg).
… probe

benches/eval.rs (criterion) and benches/allocs.rs cover BenchmarkEval
(incl. url_field), BenchmarkEvalLazyInput, BenchmarkEvalTrace,
BenchmarkParse, and BenchmarkCmpNumber by name. bench/compare.sh prints Go
and Rust ns/op and allocs/op side by side; bench/size.sh measures the
traced-eval instantiation and rlib size. README.md ships in the package.
Trace<'a> borrows its expression, value, and missing fields (accessor
methods; into_owned() detaches it). Traces are built from fragments, so
an unnamed group is a plain Vec rather than a boxed node, and missing
fields are Cow so functions can report owned names. BenchmarkEvalTrace:
22 allocations (Go 29), ~834 ns (Go ~858).
Compile prints each node's canonical expression once, reusing its
children's text, instead of re-printing every subtree; comparisons no
longer clone their operand nodes while lowering.
eval takes (ctx, input, opts) like Go. Custom functions return
Result<Val, FnError> and can report missing fields (the result is
unknown), like Go's Function.Eval. Ip/Cidr/Mac/Url::parse return
ValueParseError. Function and Macro both have with_doc/doc; Env::macro_
is macro_rule; Rule::ast() returns &Ast; TextForm is opaque; Val gets
From<&str>. Docs, doc tests, README, and DESIGN updated.
Node results are generic over a trace slot that is () when not tracing,
and keep errors and function-reported missing names in one boxed
Problem, so a result is 96 bytes instead of 136. Recovers the untraced
eval time lost when traces started borrowing (e.g. full_traversal 235 ->
216 ns, macro 117 -> 107 ns; Go 245 / 197 ns). EvalResult::missing_fields
is now an iterator.
Replaces the dynamic function API (ArgSpec, the Args accessor,
Function::new(specs, closure)). A function is
Function::new(FuncSchema::<A, R>::new(name, doc), |ctx, args| ...) where
A derives Args (one positional argument per field, rename attribute,
optional last Rest<'a> for variadic) and R: Returns gives the declared
return type statically; results may borrow from the context. The derive
lives in the new rulekit-macros crate (workspace member), re-exported
behind the default derive feature. EnvBuilder::function takes the name
from the schema and rejects duplicates. starts_with uses the same API.
ArgCount gains variadic. trybuild tests cover each derive error.
FuncSchema is a plain struct of &'static str name and doc; the argument
and return types are named on Function::new, so the closure needs no
annotations. Adds NoArgs for functions without arguments. Benches no
longer build benches/cases.rs as a benchmark.
InputValue plus lazy list/object refs so a HashMap, Vec, or JSON value is
indexed only where a rule touches it. `in` accepts a path (`"x" in tags`);
a non-array, non-CIDR literal stays a parse error.
`in` stays an array or CIDR literal, matching Go; list membership is
`contains`. Slice fields are `&dyn ListSource` via `&&[T]`, which is sized.
No raw pointers or lifetime transmutes.
Matches the first path segment to a field and reads only that field.
rename and skip attributes, generics, and a spanned error for an
unsupported field type.
kv! builds a zero-alloc heterogeneous map; lazy boxes a closure at
construction and runs it only when the field is read. derive(Input)
takes an optional context type so a lazy field is checked at the derive
site instead of for every C.
Default features url and http. Field access borrows. url::Url compares
via as_str(); reading an http::Uri itself allocates its Display form
(3 allocs), field access does not.
README and crate docs lead with derive(Input) and kv!. DESIGN records
why serde Serialize was rejected. Eval and alloc benches compare a
derived struct with the same rule over KvInput.
lazy() is the existing Lazy: one resolution per instance, single-flight
across threads, errors not stored so the next read retries. The closure
returns an owned value; a borrow of ctx is copied into the memo.
After those crates normalize, fields and comparisons follow rulekit's
URL parsed from that text. http::Uri compares borrowed parts with scheme
and host lowercased, so the comparison does not allocate.
#[rulekit(bytes)] borrows Vec<u8>, &[u8], [u8; N], Box<[u8]>, and
Cow<[u8]> as a byte string. bytes::Bytes, BytesMut, and the serde_bytes
wrappers are byte strings without the attribute. kv! uses rulekit::bytes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant