mirror of
https://github.com/elicpeter/nyx.git
synced 2026-07-03 20:41:00 +02:00
[pitboss/grind] deferred session-0015 (20260520T233019Z-6958)
This commit is contained in:
parent
ba0f83a855
commit
1e122b615e
5 changed files with 128 additions and 19 deletions
|
|
@ -207,18 +207,38 @@ pub fn extract_rust_path_placeholders(path: &str) -> Vec<String> {
|
||||||
/// [`ParamSource::PathSegment`]; `req` / `request` / `state` formals
|
/// [`ParamSource::PathSegment`]; `req` / `request` / `state` formals
|
||||||
/// fall to [`ParamSource::Implicit`]; every other formal becomes a
|
/// fall to [`ParamSource::Implicit`]; every other formal becomes a
|
||||||
/// [`ParamSource::QueryParam`].
|
/// [`ParamSource::QueryParam`].
|
||||||
|
///
|
||||||
|
/// warp's `warp::path!("users" / u32)` macro reconstructs placeholders
|
||||||
|
/// as type names (`u32`) rather than parameter names because the
|
||||||
|
/// segments are positional. When the placeholder list contains
|
||||||
|
/// typed-anonymous segments (Rust primitive type names like `u32` /
|
||||||
|
/// `String` / `Uuid`), the n-th typed-anonymous placeholder binds
|
||||||
|
/// positionally to the n-th non-implicit formal so handler signatures
|
||||||
|
/// like `fn show(id: u32)` bind `id` as a path segment instead of a
|
||||||
|
/// query param.
|
||||||
pub fn bind_rust_path_params(formals: &[String], path: &str) -> Vec<ParamBinding> {
|
pub fn bind_rust_path_params(formals: &[String], path: &str) -> Vec<ParamBinding> {
|
||||||
let placeholders = extract_rust_path_placeholders(path);
|
let placeholders = extract_rust_path_placeholders(path);
|
||||||
|
let typed_anon_count = placeholders
|
||||||
|
.iter()
|
||||||
|
.filter(|p| is_typed_anonymous_placeholder(p))
|
||||||
|
.count();
|
||||||
|
let mut non_implicit_seen = 0usize;
|
||||||
formals
|
formals
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(idx, name)| {
|
.map(|(idx, name)| {
|
||||||
let source = if is_implicit_formal(name) {
|
let source = if is_implicit_formal(name) {
|
||||||
ParamSource::Implicit
|
ParamSource::Implicit
|
||||||
} else if placeholders.iter().any(|p| p == name) {
|
|
||||||
ParamSource::PathSegment(name.clone())
|
|
||||||
} else {
|
} else {
|
||||||
ParamSource::QueryParam(name.clone())
|
let positional_slot = non_implicit_seen;
|
||||||
|
non_implicit_seen += 1;
|
||||||
|
if placeholders.iter().any(|p| p == name) {
|
||||||
|
ParamSource::PathSegment(name.clone())
|
||||||
|
} else if positional_slot < typed_anon_count {
|
||||||
|
ParamSource::PathSegment(name.clone())
|
||||||
|
} else {
|
||||||
|
ParamSource::QueryParam(name.clone())
|
||||||
|
}
|
||||||
};
|
};
|
||||||
ParamBinding {
|
ParamBinding {
|
||||||
index: idx,
|
index: idx,
|
||||||
|
|
@ -233,6 +253,30 @@ fn is_implicit_formal(name: &str) -> bool {
|
||||||
matches!(name, "req" | "request" | "state" | "ctx" | "cx" | "headers")
|
matches!(name, "req" | "request" | "state" | "ctx" | "cx" | "headers")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_typed_anonymous_placeholder(name: &str) -> bool {
|
||||||
|
matches!(
|
||||||
|
name,
|
||||||
|
"u8" | "u16"
|
||||||
|
| "u32"
|
||||||
|
| "u64"
|
||||||
|
| "u128"
|
||||||
|
| "usize"
|
||||||
|
| "i8"
|
||||||
|
| "i16"
|
||||||
|
| "i32"
|
||||||
|
| "i64"
|
||||||
|
| "i128"
|
||||||
|
| "isize"
|
||||||
|
| "f32"
|
||||||
|
| "f64"
|
||||||
|
| "bool"
|
||||||
|
| "char"
|
||||||
|
| "String"
|
||||||
|
| "str"
|
||||||
|
| "Uuid"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse Rust framework verb names (`get` / `post` / `put` / `patch`
|
/// Parse Rust framework verb names (`get` / `post` / `put` / `patch`
|
||||||
/// / `delete` / `head` / `options`). Both axum's lowercase routing
|
/// / `delete` / `head` / `options`). Both axum's lowercase routing
|
||||||
/// helpers (`get(handler)`) and actix's `web::get()` use the same
|
/// helpers (`get(handler)`) and actix's `web::get()` use the same
|
||||||
|
|
@ -869,4 +913,36 @@ mod tests {
|
||||||
find_warp_route(tree.root_node(), src, "show").expect("hit");
|
find_warp_route(tree.root_node(), src, "show").expect("hit");
|
||||||
assert!(path.contains("users"));
|
assert!(path.contains("users"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn warp_typed_anonymous_placeholder_binds_positionally() {
|
||||||
|
let formals = vec!["id".to_string()];
|
||||||
|
let bindings = bind_rust_path_params(&formals, "/users/{u32}");
|
||||||
|
assert!(matches!(bindings[0].source, ParamSource::PathSegment(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn warp_multi_typed_anonymous_placeholders_bind_positionally() {
|
||||||
|
let formals = vec!["user_id".to_string(), "post_slug".to_string()];
|
||||||
|
let bindings =
|
||||||
|
bind_rust_path_params(&formals, "/users/{u32}/posts/{String}");
|
||||||
|
assert!(matches!(bindings[0].source, ParamSource::PathSegment(_)));
|
||||||
|
assert!(matches!(bindings[1].source, ParamSource::PathSegment(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn warp_typed_anonymous_count_caps_positional_binding() {
|
||||||
|
let formals = vec!["id".to_string(), "extra".to_string()];
|
||||||
|
let bindings = bind_rust_path_params(&formals, "/users/{u32}");
|
||||||
|
assert!(matches!(bindings[0].source, ParamSource::PathSegment(_)));
|
||||||
|
assert!(matches!(bindings[1].source, ParamSource::QueryParam(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn warp_implicit_formals_skip_positional_binding() {
|
||||||
|
let formals = vec!["req".to_string(), "id".to_string()];
|
||||||
|
let bindings = bind_rust_path_params(&formals, "/users/{u32}");
|
||||||
|
assert!(matches!(bindings[0].source, ParamSource::Implicit));
|
||||||
|
assert!(matches!(bindings[1].source, ParamSource::PathSegment(_)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1101,6 +1101,7 @@ fn generate_go_mod() -> String {
|
||||||
fn emit_class_method_harness(class: &str, method: &str) -> HarnessSource {
|
fn emit_class_method_harness(class: &str, method: &str) -> HarnessSource {
|
||||||
let shim = probe_shim();
|
let shim = probe_shim();
|
||||||
let go_mod = generate_go_mod();
|
let go_mod = generate_go_mod();
|
||||||
|
let auto_registry = generate_auto_receiver_registry(class);
|
||||||
let source = format!(
|
let source = format!(
|
||||||
r##"// Nyx dynamic harness — class method (Phase 19 / Track M.1).
|
r##"// Nyx dynamic harness — class method (Phase 19 / Track M.1).
|
||||||
package main
|
package main
|
||||||
|
|
@ -1118,9 +1119,13 @@ import (
|
||||||
func nyxBuildReceiver(structName string) (reflect.Value, error) {{
|
func nyxBuildReceiver(structName string) (reflect.Value, error) {{
|
||||||
// Look up the exported type by name on the entry package. Go's
|
// Look up the exported type by name on the entry package. Go's
|
||||||
// reflect API does not expose package-level reflection over types
|
// reflect API does not expose package-level reflection over types
|
||||||
// directly, so the dispatcher uses the package's well-known
|
// directly, so the dispatcher uses a generated `NyxAutoReceivers`
|
||||||
// `NyxReceivers` registry the entry file is expected to publish.
|
// registry that the harness ships into the entry package at
|
||||||
if r, ok := entry.NyxReceivers[structName]; ok {{
|
// compile time (see `entry/nyx_auto_registry.go`). Real-world
|
||||||
|
// projects under test never need to hand-declare the registry —
|
||||||
|
// the auto-generated file references the target type by name and
|
||||||
|
// the Go compiler enforces the contract.
|
||||||
|
if r, ok := entry.NyxAutoReceivers[structName]; ok {{
|
||||||
return reflect.ValueOf(r), nil
|
return reflect.ValueOf(r), nil
|
||||||
}}
|
}}
|
||||||
return reflect.Value{{}}, fmt.Errorf("class not found: %s", structName)
|
return reflect.Value{{}}, fmt.Errorf("class not found: %s", structName)
|
||||||
|
|
@ -1180,11 +1185,40 @@ func main() {{
|
||||||
source,
|
source,
|
||||||
filename: "main.go".to_owned(),
|
filename: "main.go".to_owned(),
|
||||||
command: vec!["./nyx_harness".to_owned()],
|
command: vec!["./nyx_harness".to_owned()],
|
||||||
extra_files: vec![("go.mod".to_owned(), go_mod)],
|
extra_files: vec![
|
||||||
|
("go.mod".to_owned(), go_mod),
|
||||||
|
(
|
||||||
|
"entry/nyx_auto_registry.go".to_owned(),
|
||||||
|
auto_registry,
|
||||||
|
),
|
||||||
|
],
|
||||||
entry_subpath: Some("entry/entry.go".to_owned()),
|
entry_subpath: Some("entry/entry.go".to_owned()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generate an `entry/nyx_auto_registry.go` source that publishes a
|
||||||
|
/// `NyxAutoReceivers` map keyed by the target class name to a
|
||||||
|
/// zero-constructed instance. The generated file lives in package
|
||||||
|
/// `entry` so it can reference `class` by bare identifier without
|
||||||
|
/// re-exporting through the harness package. Compile-time enforcement
|
||||||
|
/// of the contract is delegated to the Go compiler — if the entry
|
||||||
|
/// package does not declare `class`, the build fails with a clear
|
||||||
|
/// `undefined: <class>` error.
|
||||||
|
fn generate_auto_receiver_registry(class: &str) -> String {
|
||||||
|
format!(
|
||||||
|
r##"// Code generated by Nyx — DO NOT EDIT.
|
||||||
|
package entry
|
||||||
|
|
||||||
|
// NyxAutoReceivers maps a class name to a zero-constructed instance
|
||||||
|
// the dynamic harness uses to reflect on methods at runtime.
|
||||||
|
var NyxAutoReceivers = map[string]interface{{}}{{
|
||||||
|
"{class}": {class}{{}},
|
||||||
|
}}
|
||||||
|
"##,
|
||||||
|
class = class,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Phase 20 (Track M.2) — message-handler harness for Go.
|
/// Phase 20 (Track M.2) — message-handler harness for Go.
|
||||||
///
|
///
|
||||||
/// The entry package is expected to declare a top-level handler
|
/// The entry package is expected to declare a top-level handler
|
||||||
|
|
|
||||||
|
|
@ -173,8 +173,15 @@ fn class_method_java_emits_reflective_dispatch() {
|
||||||
fn class_method_go_uses_reflect_receivers_registry() {
|
fn class_method_go_uses_reflect_receivers_registry() {
|
||||||
let spec = make_spec(Lang::Go);
|
let spec = make_spec(Lang::Go);
|
||||||
let h = lang::emit(&spec).expect("emit ok");
|
let h = lang::emit(&spec).expect("emit ok");
|
||||||
assert!(h.source.contains("entry.NyxReceivers"));
|
assert!(h.source.contains("entry.NyxAutoReceivers"));
|
||||||
assert!(h.source.contains("MethodByName"));
|
assert!(h.source.contains("MethodByName"));
|
||||||
|
let registry = h
|
||||||
|
.extra_files
|
||||||
|
.iter()
|
||||||
|
.find(|(name, _)| name == "entry/nyx_auto_registry.go")
|
||||||
|
.expect("auto registry emitted");
|
||||||
|
assert!(registry.1.contains("NyxAutoReceivers"));
|
||||||
|
assert!(registry.1.contains("UserService{}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,3 @@ func (UserService) Run(input string) string {
|
||||||
out, _ := exec.Command("/bin/echo", input).Output()
|
out, _ := exec.Command("/bin/echo", input).Output()
|
||||||
return string(out)
|
return string(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
var NyxReceivers = map[string]interface{}{
|
|
||||||
"UserService": UserService{},
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
// Phase 19 (Track M.1) — class-method vuln fixture for Go.
|
// Phase 19 (Track M.1) — class-method vuln fixture for Go.
|
||||||
//
|
//
|
||||||
// UserService.Run accepts user input and passes it to `sh -c` so the
|
// UserService.Run accepts user input and passes it to `sh -c` so the
|
||||||
// shell interprets it. The fixture publishes its instance through the
|
// shell interprets it. The harness compiles in a generated
|
||||||
// well-known `NyxReceivers` registry the harness uses to construct
|
// `nyx_auto_registry.go` that publishes `UserService{}` so reflection
|
||||||
// receivers reflectively.
|
// works without a hand-rolled registry in the fixture.
|
||||||
package entry
|
package entry
|
||||||
|
|
||||||
import "os/exec"
|
import "os/exec"
|
||||||
|
|
@ -15,7 +15,3 @@ func (UserService) Run(input string) string {
|
||||||
out, _ := exec.Command("sh", "-c", "echo "+input).Output()
|
out, _ := exec.Command("sh", "-c", "echo "+input).Output()
|
||||||
return string(out)
|
return string(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
var NyxReceivers = map[string]interface{}{
|
|
||||||
"UserService": UserService{},
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue