Skip to content

Commit efe8014

Browse files
committed
Call framework host functions from RustScript examples
1 parent f8c86c3 commit efe8014

4 files changed

Lines changed: 136 additions & 20 deletions

File tree

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ Standalone Pingora integration demo for `pd-vm` / RustScript.
66

77
A Pingora gateway can keep the framework and proxy code compiled while moving request policy into RustScript:
88

9-
- input from `pingora::http::RequestHeader`: method, path, `x-user-tier`
9+
- input from real `pingora::http::RequestHeader`: method, path, `x-user-tier`
10+
- RustScript calls host functions backed by Pingora request data:
11+
- `pingora_header(name) -> string`
12+
- `pingora_method_is(expected) -> bool`
1013
- scripted output: `allow`, `route:<upstream>`, or `deny:<status>:<reason>`
1114
- optional conversion of deny decisions back into `pingora::http::ResponseHeader`
1215

scripts/gateway_policy.rss

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
fn pingora_header(name) -> string;
2+
fn pingora_method_is(expected) -> bool;
13
fn gateway_decision(decision) -> string;
24

5+
let tier = pingora_header("x-user-tier");
36
let blocked_admin = path == "/admin" && tier != "pro";
4-
let canary = path == "/canary" && method == "GET";
7+
let canary = path == "/canary" && pingora_method_is("GET");
58
let decision = if blocked_admin => {
69
"deny:403:upgrade required"
710
} else if canary => {

src/lib.rs

Lines changed: 86 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,14 @@ pub struct ScriptedGatewayPolicy {
1616
impl ScriptedGatewayPolicy {
1717
pub fn from_source(source: impl Into<String>) -> Result<Self, String> {
1818
let source = source.into();
19-
run_string(&wrap_request_source(&source, "GET", "/", ""))?;
19+
let request = RequestHeader::build("GET", b"/", None)
20+
.map_err(|err| format!("failed to build Pingora request: {err}"))?;
21+
run_string(&wrap_request_source(&source, &request), &request)?;
2022
Ok(Self { source })
2123
}
2224

2325
pub fn evaluate_request(&self, request: &RequestHeader) -> Result<GatewayDecision, String> {
24-
let method = request.method.as_str();
25-
let path = String::from_utf8_lossy(request.raw_path());
26-
let tier = request
27-
.headers
28-
.get("x-user-tier")
29-
.and_then(|value| value.to_str().ok())
30-
.unwrap_or("");
31-
let output = run_string(&wrap_request_source(&self.source, method, &path, tier))?;
26+
let output = run_string(&wrap_request_source(&self.source, request), request)?;
3227
parse_decision(&output)
3328
}
3429

@@ -69,23 +64,57 @@ fn parse_decision(output: &str) -> Result<GatewayDecision, String> {
6964
Err(format!("unknown gateway decision '{output}'"))
7065
}
7166

72-
fn wrap_request_source(policy: &str, method: &str, path: &str, tier: &str) -> String {
67+
fn wrap_request_source(policy: &str, request: &RequestHeader) -> String {
68+
let method = request.method.as_str();
69+
let path = String::from_utf8_lossy(request.raw_path());
7370
format!(
74-
"let method = {};\nlet path = {};\nlet tier = {};\n{}",
71+
"let method = {};\nlet path = {};\n{}",
7572
rss_string(method),
76-
rss_string(path),
77-
rss_string(tier),
73+
rss_string(&path),
7874
policy
7975
)
8076
}
8177

82-
fn run_string(source: &str) -> Result<String, String> {
83-
match run_value(source)? {
78+
fn run_string(source: &str, request: &RequestHeader) -> Result<String, String> {
79+
match run_value(source, request)? {
8480
Value::String(value) => Ok(value.as_str().to_string()),
8581
other => Err(format!("script returned {other:?}; expected string")),
8682
}
8783
}
8884

85+
#[derive(Debug, Clone)]
86+
struct PingoraRequestSnapshot {
87+
method: String,
88+
headers: Vec<(String, String)>,
89+
}
90+
91+
impl PingoraRequestSnapshot {
92+
fn from_request(request: &RequestHeader) -> Self {
93+
let headers = request
94+
.headers
95+
.iter()
96+
.filter_map(|(name, value)| {
97+
Some((
98+
name.as_str().to_ascii_lowercase(),
99+
value.to_str().ok()?.to_string(),
100+
))
101+
})
102+
.collect();
103+
Self {
104+
method: request.method.as_str().to_string(),
105+
headers,
106+
}
107+
}
108+
109+
fn header(&self, name: &str) -> String {
110+
let needle = name.to_ascii_lowercase();
111+
self.headers
112+
.iter()
113+
.find_map(|(header_name, value)| (header_name == &needle).then(|| value.clone()))
114+
.unwrap_or_default()
115+
}
116+
}
117+
89118
struct GatewayDecisionHost;
90119

91120
impl HostFunction for GatewayDecisionHost {
@@ -99,10 +128,51 @@ impl HostFunction for GatewayDecisionHost {
99128
}
100129
}
101130

102-
fn run_value(source: &str) -> Result<Value, String> {
131+
struct PingoraHeaderHost {
132+
request: PingoraRequestSnapshot,
133+
}
134+
135+
impl HostFunction for PingoraHeaderHost {
136+
fn call(&mut self, _vm: &mut Vm, args: &[Value]) -> Result<CallOutcome, VmError> {
137+
match args {
138+
[Value::String(name)] => Ok(CallOutcome::Return(CallReturn::one(Value::string(
139+
self.request.header(name.as_str()),
140+
)))),
141+
_ => Err(VmError::TypeMismatch("header name string")),
142+
}
143+
}
144+
}
145+
146+
struct PingoraMethodIsHost {
147+
request: PingoraRequestSnapshot,
148+
}
149+
150+
impl HostFunction for PingoraMethodIsHost {
151+
fn call(&mut self, _vm: &mut Vm, args: &[Value]) -> Result<CallOutcome, VmError> {
152+
match args {
153+
[Value::String(expected)] => Ok(CallOutcome::Return(CallReturn::one(Value::Bool(
154+
self.request.method == expected.as_str(),
155+
)))),
156+
_ => Err(VmError::TypeMismatch("method string")),
157+
}
158+
}
159+
}
160+
161+
fn run_value(source: &str, request: &RequestHeader) -> Result<Value, String> {
103162
let compiled = compile_source(source).map_err(|err| err.to_string())?;
163+
let snapshot = PingoraRequestSnapshot::from_request(request);
104164
let mut vm = Vm::new(compiled.program);
105165
vm.bind_function("gateway_decision", Box::new(GatewayDecisionHost));
166+
vm.bind_function(
167+
"pingora_header",
168+
Box::new(PingoraHeaderHost {
169+
request: snapshot.clone(),
170+
}),
171+
);
172+
vm.bind_function(
173+
"pingora_method_is",
174+
Box::new(PingoraMethodIsHost { request: snapshot }),
175+
);
106176
let status = vm.run().map_err(|err| err.to_string())?;
107177
if status != VmStatus::Halted {
108178
return Err(format!("script did not halt: {status:?}"));

tests/gateway_policy.rs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use pretty_assertions::assert_eq;
33
use rustscript_pingora_gateway_policy::{GatewayDecision, ScriptedGatewayPolicy};
44

55
#[test]
6-
fn rustscript_denies_pingora_admin_request_for_free_tier() {
6+
fn rustscript_calls_pingora_request_header_functions_for_free_tier() {
77
let policy = ScriptedGatewayPolicy::from_source(include_str!("../scripts/gateway_policy.rss"))
88
.expect("policy should compile");
99
let mut request = RequestHeader::build("GET", b"/admin", None).expect("request should build");
@@ -25,7 +25,7 @@ fn rustscript_denies_pingora_admin_request_for_free_tier() {
2525
}
2626

2727
#[test]
28-
fn rustscript_routes_pingora_canary_request_without_framework_fork() {
28+
fn rustscript_calls_pingora_method_function_for_canary_route() {
2929
let policy = ScriptedGatewayPolicy::from_source(include_str!("../scripts/gateway_policy.rss"))
3030
.expect("policy should compile");
3131
let request = RequestHeader::build("GET", b"/canary", None).expect("request should build");
@@ -39,3 +39,43 @@ fn rustscript_routes_pingora_canary_request_without_framework_fork() {
3939
GatewayDecision::Route("canary-upstream".to_string())
4040
);
4141
}
42+
43+
#[test]
44+
fn rustscript_can_call_pingora_host_functions_from_inline_policy() {
45+
let policy = ScriptedGatewayPolicy::from_source(
46+
r#"
47+
fn pingora_header(name) -> string;
48+
fn pingora_method_is(expected) -> bool;
49+
fn gateway_decision(decision) -> string;
50+
51+
let tier = pingora_header("x-user-tier");
52+
let blocked_admin = path == "/admin" && tier != "pro";
53+
let canary = path == "/canary" && pingora_method_is("GET");
54+
let decision = if blocked_admin => {
55+
"deny:403:upgrade required"
56+
} else if canary => {
57+
"route:canary-upstream"
58+
} else => {
59+
"allow"
60+
};
61+
gateway_decision(decision);
62+
"#,
63+
)
64+
.expect("policy should compile");
65+
let mut request = RequestHeader::build("GET", b"/admin", None).expect("request should build");
66+
request
67+
.insert_header("x-user-tier", "free")
68+
.expect("header should insert");
69+
70+
let decision = policy
71+
.evaluate_request(&request)
72+
.expect("policy should evaluate");
73+
74+
assert_eq!(
75+
decision,
76+
GatewayDecision::Deny {
77+
status: 403,
78+
reason: "upgrade required".to_string(),
79+
}
80+
);
81+
}

0 commit comments

Comments
 (0)