Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 6f2bf6d

Browse files
committedJun 6, 2025·
Improve docs and tests, and add instance conditions and href rewriting
1 parent ce25f0a commit 6f2bf6d

27 files changed

+893
-268
lines changed
 

‎README.md‎

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ console.log(response.body.toString())
6262
* `docroot` {String} Document root for PHP. **Default:** process.cwd()
6363
* `throwRequestErrors` {Boolean} Throw request errors rather than returning
6464
responses with error codes. **Default:** false
65+
* `rewriter` {Rewriter} Optional rewrite rules. **Default:** `undefined`
6566
* Returns: {Php}
6667

6768
Construct a new PHP instance to which to dispatch requests.
@@ -560,9 +561,80 @@ headers.forEach((value, name, headers) => {
560561
})
561562
```
562563

564+
### `new Rewriter(input)`
565+
566+
* `rules` {Array} The set of rewriting rules to apply to each request
567+
* `operation` {String} Operation type (`and` or `or`) **Default:** `and`
568+
* `conditions` {Array} Conditions to match against the request
569+
* `type` {String} Condition type
570+
* `args` {String} Arguments to pass to condition constructor
571+
* `rewriters` {Array} Rewrites to apply if the conditions match
572+
* `type` {String} Rewriter type
573+
* `args` {String} Arguments to pass to rewriter constructor
574+
* Returns: {Rewriter}
575+
576+
Construct a Rewriter to rewrite requests before they are dispatched to PHP.
577+
578+
```js
579+
import { Rewriter } from '@platformatic/php-node'
580+
581+
const rewriter = new Rewriter([{
582+
conditions: [{
583+
type: 'header',
584+
args: ['User-Agent', '^(Mozilla|Chrome)']
585+
}],
586+
rewriters: [{
587+
type: 'path',
588+
args: ['^/old-path/(.*)$', '/new-path/$1']
589+
}]
590+
}])
591+
```
592+
593+
#### Conditions
594+
595+
There are several types of conditions which may be used to match against the
596+
request. Each condition type has a set of arguments which are passed to the
597+
constructor of the condition. The condition will be evaluated against the
598+
request and if it matches, the rewriters will be applied.
599+
600+
The available condition types are:
601+
602+
- `exists` Matches if request path exists in docroot.
603+
- `not_exists` Matches if request path does not exist in docroot.
604+
- `header(name, pattern)` Matches named header against a pattern.
605+
- `name` {String} The name of the header to match.
606+
- `pattern` {String} The regex pattern to match against the header value.
607+
- `method(pattern)` Matches request method against a pattern.
608+
- `pattern` {String} The regex pattern to match against the HTTP method.
609+
- `path(pattern)`: Matches request path against a pattern.
610+
- `pattern` {String} The regex pattern to match against the request path.
611+
612+
#### Rewriters
613+
614+
There are several types of rewriters which may be used to rewrite the request
615+
before it is dispatched to PHP. Each rewriter type has a set of arguments which
616+
are passed to the constructor of the rewriter. The rewriter will be applied to
617+
the request if the conditions match.
618+
619+
The available rewriter types are:
620+
621+
- `header(name, replacement)` Sets a named header to a given replacement.
622+
- `name` {String} The name of the header to set.
623+
- `replacement` {String} The replacement string to use for named header.
624+
- `href(pattern, replacement)` Rewrites request path, query, and fragment to
625+
given replacement.
626+
- `pattern` {String} The regex pattern to match against the request path.
627+
- `replacement` {String} The replacement string to use for request path.
628+
- `method(replacement)` Sets the request method to a given replacement.
629+
- `replacement` {String} The replacement string to use for request method.
630+
- `path(pattern, replacement)` Rewrites request path to given replacement.
631+
- `pattern` {String} The regex pattern to match against the request path.
632+
- `replacement` {String} The replacement string to use for request path.
633+
563634
## Contributing
564635

565-
This project is part of the [Platformatic](https://github.com/platformatic) ecosystem. Please refer to the main repository for contribution guidelines.
636+
This project is part of the [Platformatic](https://github.com/platformatic)
637+
ecosystem. Please refer to the main repository for contribution guidelines.
566638

567639
## License
568640

‎__test__/handler.spec.mjs‎

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -175,19 +175,13 @@ test('Accept rewriter', async (t) => {
175175
})
176176
t.teardown(() => mockroot.clean())
177177

178-
const rewrite = new Rewriter([
178+
const rewriter = new Rewriter([
179179
{
180180
conditions: [
181-
{
182-
type: 'path',
183-
args: ['^/rewrite_me$']
184-
}
181+
{ type: 'path', args: ['^/rewrite_me$'] }
185182
],
186183
rewriters: [
187-
{
188-
type: 'path',
189-
args: ['^/rewrite_me$', '/index.php']
190-
}
184+
{ type: 'path', args: ['^/rewrite_me$', '/index.php'] }
191185
]
192186
}
193187
])
@@ -196,7 +190,7 @@ test('Accept rewriter', async (t) => {
196190
argv: process.argv,
197191
docroot: mockroot.path,
198192
throwRequestErrors: true,
199-
rewrite
193+
rewriter
200194
})
201195

202196
const req = new Request({

‎__test__/rewriter.spec.mjs‎

Lines changed: 261 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,287 @@ import test from 'ava'
22

33
import { Request, Rewriter } from '../index.js'
44

5-
test('rewrites URLs', (t) => {
5+
const docroot = import.meta.dirname
6+
7+
test('existence condition', (t) => {
68
const req = new Request({
79
method: 'GET',
8-
url: 'http://example.com/index.php',
10+
url: 'http://example.com/util.mjs',
911
headers: {
1012
TEST: ['foo']
1113
}
1214
})
1315

1416
const rewriter = new Rewriter([
1517
{
16-
operation: 'and',
1718
conditions: [
19+
{ type: 'exists' }
20+
],
21+
rewriters: [
1822
{
1923
type: 'path',
20-
args: ['^/index.php$']
21-
},
22-
{
23-
type: 'header',
24-
args: ['TEST', '^foo$']
24+
args: ['.*', '/404']
2525
}
26+
]
27+
}
28+
])
29+
30+
t.is(rewriter.rewrite(req, docroot).url, 'http://example.com/404')
31+
})
32+
33+
test('non-existence condition', (t) => {
34+
const req = new Request({
35+
method: 'GET',
36+
url: 'http://example.com/index.php',
37+
headers: {
38+
TEST: ['foo']
39+
}
40+
})
41+
42+
const rewriter = new Rewriter([
43+
{
44+
conditions: [
45+
{ type: 'not_exists' }
2646
],
2747
rewriters: [
2848
{
2949
type: 'path',
30-
args: ['^(/index.php)$', '/foo$1']
50+
args: ['.*', '/404']
3151
}
3252
]
3353
}
3454
])
3555

36-
t.is(rewriter.rewrite(req).url, 'http://example.com/foo/index.php')
56+
t.is(rewriter.rewrite(req, docroot).url, 'http://example.com/404')
57+
})
58+
59+
test('condition groups - AND', (t) => {
60+
const rewriter = new Rewriter([{
61+
conditions: [
62+
{ type: 'header', args: ['TEST', 'foo'] },
63+
{ type: 'path', args: ['^(/index.php)$'] }
64+
],
65+
rewriters: [
66+
{ type: 'path', args: ['^(/index.php)$', '/foo$1'] }
67+
]
68+
}])
69+
70+
// Both conditions match, so rewrite is applied
71+
{
72+
const req = new Request({
73+
method: 'GET',
74+
url: 'http://example.com/index.php',
75+
headers: {
76+
TEST: ['foo']
77+
}
78+
})
79+
80+
t.is(
81+
rewriter.rewrite(req, docroot).url,
82+
'http://example.com/foo/index.php'
83+
)
84+
}
85+
86+
// Header condition does not match, so rewrite is not applied
87+
{
88+
const req = new Request({
89+
method: 'GET',
90+
url: 'http://example.com/index.php'
91+
})
92+
93+
t.is(
94+
rewriter.rewrite(req, docroot).url,
95+
'http://example.com/index.php'
96+
)
97+
}
98+
99+
// Path condition does not match, so rewrite is not applied
100+
{
101+
const req = new Request({
102+
method: 'GET',
103+
url: 'http://example.com/nope.php',
104+
headers: {
105+
TEST: ['foo']
106+
}
107+
})
108+
109+
t.is(
110+
rewriter.rewrite(req, docroot).url,
111+
'http://example.com/nope.php'
112+
)
113+
}
114+
})
115+
116+
test('condition groups - OR', (t) => {
117+
const rewriter = new Rewriter([{
118+
operation: 'or',
119+
conditions: [
120+
{ type: 'method', args: ['GET'] },
121+
{ type: 'path', args: ['^(/index.php)$'] }
122+
],
123+
rewriters: [
124+
{ type: 'path', args: ['^(.*)$', '/foo$1'] }
125+
]
126+
}])
127+
128+
// Both conditions match, so rewrite is applied
129+
{
130+
const req = new Request({
131+
url: 'http://example.com/index.php'
132+
})
133+
134+
t.is(
135+
rewriter.rewrite(req, docroot).url,
136+
'http://example.com/foo/index.php'
137+
)
138+
}
139+
140+
// Path condition matches, so rewrite is applied
141+
{
142+
const req = new Request({
143+
url: 'http://example.com/index.php'
144+
})
145+
146+
t.is(
147+
rewriter.rewrite(req, docroot).url,
148+
'http://example.com/foo/index.php'
149+
)
150+
}
151+
152+
// Header condition matches, so rewrite is applied
153+
{
154+
const req = new Request({
155+
url: 'http://example.com/nope.php'
156+
})
157+
158+
t.is(
159+
rewriter.rewrite(req, docroot).url,
160+
'http://example.com/foo/nope.php'
161+
)
162+
}
163+
164+
// Neither condition matches, so rewrite is not applied
165+
{
166+
const req = new Request({
167+
method: 'POST',
168+
url: 'http://example.com/nope.php'
169+
})
170+
171+
t.is(
172+
rewriter.rewrite(req, docroot).url,
173+
'http://example.com/nope.php'
174+
)
175+
}
176+
})
177+
178+
test('header rewriting', (t) => {
179+
const rewriter = new Rewriter([{
180+
rewriters: [
181+
{ type: 'header', args: ['TEST', '(.*)', '${1}bar'] }
182+
]
183+
}])
184+
185+
const req = new Request({
186+
method: 'GET',
187+
url: 'http://example.com/index.php',
188+
headers: {
189+
TEST: ['foo']
190+
}
191+
})
192+
193+
t.is(rewriter.rewrite(req, docroot).headers.get('TEST'), 'foobar')
194+
})
195+
196+
test('href rewriting', (t) => {
197+
const rewriter = new Rewriter([{
198+
rewriters: [
199+
{ type: 'href', args: [ '^(.*)$', '/index.php?route=${1}' ] }
200+
]
201+
}])
202+
203+
const req = new Request({
204+
url: 'http://example.com/foo/bar'
205+
})
206+
207+
t.is(
208+
rewriter.rewrite(req, docroot).url,
209+
'http://example.com/index.php?route=/foo/bar'
210+
)
211+
})
212+
213+
test('method rewriting', (t) => {
214+
const rewriter = new Rewriter([{
215+
rewriters: [
216+
{ type: 'method', args: ['GET', 'POST'] }
217+
]
218+
}])
219+
220+
const req = new Request({
221+
url: 'http://example.com/index.php'
222+
})
223+
224+
t.is(rewriter.rewrite(req, docroot).method, 'POST')
225+
})
226+
227+
test('path rewriting', (t) => {
228+
const rewriter = new Rewriter([{
229+
rewriters: [
230+
{ type: 'path', args: ['^(/index.php)$', '/foo$1'] }
231+
]
232+
}])
233+
234+
const req = new Request({
235+
method: 'GET',
236+
url: 'http://example.com/index.php',
237+
headers: {
238+
TEST: ['foo']
239+
}
240+
})
241+
242+
t.is(rewriter.rewrite(req, docroot).url, 'http://example.com/foo/index.php')
243+
})
244+
245+
test('rewriter sequencing', (t) => {
246+
const rewriter = new Rewriter([{
247+
conditions: [
248+
{ type: 'path', args: ['^(/index.php)$'] }
249+
],
250+
rewriters: [
251+
{ type: 'path', args: ['^(/index.php)$', '/bar$1'] },
252+
{ type: 'path', args: ['^(/bar)', '/foo$1'] }
253+
]
254+
}])
255+
256+
// Condition matches, and both rewriters are applied in sequence
257+
{
258+
const req = new Request({
259+
method: 'GET',
260+
url: 'http://example.com/index.php',
261+
headers: {
262+
TEST: ['foo']
263+
}
264+
})
265+
266+
t.is(
267+
rewriter.rewrite(req, docroot).url,
268+
'http://example.com/foo/bar/index.php'
269+
)
270+
}
271+
272+
// Condition does not match, so no rewrites are applied even if the second
273+
// rewriter would match
274+
{
275+
const req = new Request({
276+
method: 'GET',
277+
url: 'http://example.com/bar/baz.php',
278+
headers: {
279+
TEST: ['foo']
280+
}
281+
})
282+
283+
t.is(
284+
rewriter.rewrite(req, docroot).url,
285+
'http://example.com/bar/baz.php'
286+
)
287+
}
37288
})

‎crates/lang_handler/src/handler.rs‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,49 @@
1+
//! # Handling Requests
2+
//!
3+
//! The `Handler` trait is used to define how requests are handled. It provides
4+
//! a method `handle` which takes a `Request` and returns a `Response`. This
5+
//! allows you to implement custom logic for handling requests, such as routing
6+
//! them to different services or processing them in some way.
7+
//!
8+
//! ```rust
9+
//! use lang_handler::{
10+
//! Handler,
11+
//! Request,
12+
//! RequestBuilder,
13+
//! Response,
14+
//! ResponseBuilder
15+
//! };
16+
//!
17+
//! pub struct EchoServer;
18+
//! impl Handler for EchoServer {
19+
//! type Error = String;
20+
//! fn handle(&self, request: Request) -> Result<Response, Self::Error> {
21+
//! let response = Response::builder()
22+
//! .status(200)
23+
//! .body(request.body())
24+
//! .build();
25+
//!
26+
//! Ok(response)
27+
//! }
28+
//! }
29+
//!
30+
//! let handler = EchoServer;
31+
//!
32+
//! let request = Request::builder()
33+
//! .method("POST")
34+
//! .url("http://example.com")
35+
//! .header("Accept", "application/json")
36+
//! .body("Hello, world!")
37+
//! .build()
38+
//! .expect("should build request");
39+
//!
40+
//! let response = handler.handle(request)
41+
//! .expect("should handle request");
42+
//!
43+
//! assert_eq!(response.status(), 200);
44+
//! assert_eq!(response.body(), "Hello, world!");
45+
//! ```
46+
147
use super::{Request, Response};
248

349
/// Enables a type to support handling HTTP requests.

‎crates/lang_handler/src/headers.rs‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,50 @@
1+
//! # Managing Headers
2+
//!
3+
//! The `Headers` type provides methods to read and manipulate HTTP headers.
4+
//!
5+
//! ```rust
6+
//! use lang_handler::Headers;
7+
//!
8+
//! // Setting and getting headers
9+
//! let mut headers = Headers::new();
10+
//! headers.set("Content-Type", "application/json");
11+
//! assert_eq!(headers.get("Content-Type"), Some("application/json".to_string()));
12+
//!
13+
//! // Checking if a header exists
14+
//! assert!(headers.has("Content-Type"));
15+
//!
16+
//! // Removing headers
17+
//! headers.remove("Content-Type");
18+
//! assert_eq!(headers.get("Content-Type"), None);
19+
//!
20+
//! // Adding multiple values to a header
21+
//! headers.add("Set-Cookie", "sessionid=abc123");
22+
//! headers.add("Set-Cookie", "userid=42");
23+
//!
24+
//! // Iterating over headers
25+
//! for (name, value) in headers.iter() {
26+
//! println!("{}: {:?}", name, value);
27+
//! }
28+
//!
29+
//! // Getting all values for a header
30+
//! let cookies = headers.get_all("Set-Cookie");
31+
//! assert_eq!(cookies, vec!["sessionid=abc123", "userid=42"]);
32+
//!
33+
//! // Getting a set of headers as a string line
34+
//! headers.add("Accept", "text/plain");
35+
//! headers.add("Accept", "application/json");
36+
//! let accept_header = headers.get_line("Accept");
37+
//!
38+
//! // Counting header lines
39+
//! assert!(headers.len() > 0);
40+
//!
41+
//! // Clearing all headers
42+
//! headers.clear();
43+
//!
44+
//! // Checking if headers are empty
45+
//! assert!(headers.is_empty());
46+
//! ```
47+
148
use std::collections::{hash_map::Entry, HashMap};
249

350
#[derive(Debug, Clone)]

‎crates/lang_handler/src/lib.rs‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
//! # HTTP Request Management
2+
//!
3+
//! Lang Handler is a library intended for managing HTTP requests between
4+
//! multiple languages. It provides types for representing Headers, Request,
5+
//! and Response, as well as providing a Handler trait for dispatching
6+
//! Request objects into some other system which produces a Response.
7+
//! This may be another language runtime, or it could be a server application
8+
//! directly in Rust.
9+
110
#[cfg(feature = "c")]
211
mod ffi;
312
mod handler;
@@ -11,7 +20,7 @@ mod test;
1120
pub use ffi::*;
1221
pub use handler::Handler;
1322
pub use headers::{Header, Headers};
14-
pub use request::{Request, RequestBuilder};
23+
pub use request::{Request, RequestBuilder, RequestBuilderException};
1524
pub use response::{Response, ResponseBuilder};
1625
pub use test::{MockRoot, MockRootBuilder};
1726
pub use url::Url;

‎crates/lang_handler/src/request.rs‎

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,45 @@
1+
//! # Building a Request
2+
//!
3+
//! The `Request` type provides a `builder` method which allows you to
4+
//! construct a `Request` object using a fluent API. This allows you to
5+
//! set the URL, HTTP method, headers, body, and other properties of the
6+
//! request in a clear and concise manner.
7+
//!
8+
//! # Example
9+
//!
10+
//! ```rust
11+
//! use lang_handler::{Request, RequestBuilder};
12+
//!
13+
//! let request = Request::builder()
14+
//! .method("GET")
15+
//! .url("http://example.com")
16+
//! .header("Accept", "application/json")
17+
//! .build()
18+
//! .expect("should build request");
19+
//! ```
20+
//!
21+
//! # Reading a Request
22+
//!
23+
//! The `Request` type also provides methods to read the URL, HTTP method,
24+
//! headers, and body of the request. This allows you to access the properties
25+
//! of the request in a straightforward manner.
26+
//!
27+
//! ```rust
28+
//! # use lang_handler::Request;
29+
//! #
30+
//! # let request = Request::builder()
31+
//! # .method("GET")
32+
//! # .url("http://example.com")
33+
//! # .header("Accept", "application/json")
34+
//! # .build()
35+
//! # .expect("should build request");
36+
//! #
37+
//! assert_eq!(request.method(), "GET");
38+
//! assert_eq!(request.url().to_string(), "http://example.com/");
39+
//! assert_eq!(request.headers().get("Accept"), Some("application/json".to_string()));
40+
//! assert_eq!(request.body(), "");
41+
//! ```
42+
143
use std::{fmt::Debug, net::SocketAddr};
244

345
use bytes::{Bytes, BytesMut};
@@ -276,7 +318,7 @@ impl Request {
276318
}
277319

278320
/// Errors which may be produced when building a Request from a RequestBuilder.
279-
#[derive(Debug, PartialEq)]
321+
#[derive(Debug, PartialEq, Eq, Hash)]
280322
pub enum RequestBuilderException {
281323
/// Url is required
282324
UrlMissing,

‎crates/lang_handler/src/response.rs‎

Lines changed: 67 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,45 @@
1+
//! # Building a Response
2+
//!
3+
//! The `Response` type also provides a `builder` method which allows you to
4+
//! construct a `Response` object using a fluent API. This allows you to
5+
//! set the status code, headers, body, and other properties of the
6+
//! response in a clear and concise manner.
7+
//!
8+
//! ```rust
9+
//! use lang_handler::{Response, ResponseBuilder};
10+
//!
11+
//! let response = Response::builder()
12+
//! .status(200)
13+
//! .header("Content-Type", "application/json")
14+
//! .body("{\"message\": \"Hello, world!\"}")
15+
//! .log("This is a log message")
16+
//! .exception("This is an exception message")
17+
//! .build();
18+
//! ```
19+
//!
20+
//! # Reading a Response
21+
//!
22+
//! The `Response` type provides methods to read the status code, headers,
23+
//! body, log, and exception of the response.
24+
//!
25+
//! ```rust
26+
//! # use lang_handler::{Response, ResponseBuilder};
27+
//! #
28+
//! # let response = Response::builder()
29+
//! # .status(200)
30+
//! # .header("Content-Type", "text/plain")
31+
//! # .body("Hello, World!")
32+
//! # .log("This is a log message")
33+
//! # .exception("This is an exception message")
34+
//! # .build();
35+
//! #
36+
//! assert_eq!(response.status(), 200);
37+
//! assert_eq!(response.headers().get("Content-Type"), Some("text/plain".to_string()));
38+
//! assert_eq!(response.body(), "Hello, World!");
39+
//! assert_eq!(response.log(), "This is a log message");
40+
//! assert_eq!(response.exception(), Some(&"This is an exception message".to_string()));
41+
//! ```
42+
143
use bytes::{Bytes, BytesMut};
244

345
use super::Headers;
@@ -250,6 +292,31 @@ impl ResponseBuilder {
250292
}
251293
}
252294

295+
/// Builds the response.
296+
///
297+
/// # Example
298+
///
299+
/// ```
300+
/// use lang_handler::ResponseBuilder;
301+
///
302+
/// let response = ResponseBuilder::new()
303+
/// .build();
304+
///
305+
/// assert_eq!(response.status(), 200);
306+
/// assert_eq!(response.body(), "");
307+
/// assert_eq!(response.log(), "");
308+
/// assert_eq!(response.exception(), None);
309+
/// ```
310+
pub fn build(&self) -> Response {
311+
Response {
312+
status: self.status.unwrap_or(200),
313+
headers: self.headers.clone(),
314+
body: self.body.clone().freeze(),
315+
log: self.log.clone().freeze(),
316+
exception: self.exception.clone(),
317+
}
318+
}
319+
253320
/// Creates a new response builder that extends the given response.
254321
///
255322
/// # Example
@@ -385,31 +452,6 @@ impl ResponseBuilder {
385452
self.exception = Some(exception.into());
386453
self
387454
}
388-
389-
/// Builds the response.
390-
///
391-
/// # Example
392-
///
393-
/// ```
394-
/// use lang_handler::ResponseBuilder;
395-
///
396-
/// let response = ResponseBuilder::new()
397-
/// .build();
398-
///
399-
/// assert_eq!(response.status(), 200);
400-
/// assert_eq!(response.body(), "");
401-
/// assert_eq!(response.log(), "");
402-
/// assert_eq!(response.exception(), None);
403-
/// ```
404-
pub fn build(&self) -> Response {
405-
Response {
406-
status: self.status.unwrap_or(200),
407-
headers: self.headers.clone(),
408-
body: self.body.clone().freeze(),
409-
log: self.log.clone().freeze(),
410-
exception: self.exception.clone(),
411-
}
412-
}
413455
}
414456

415457
impl Default for ResponseBuilder {
Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
1+
use std::path::Path;
2+
13
use super::{Condition, Request};
24

35
impl<F> Condition for F
46
where
5-
F: Fn(&Request) -> bool + Sync + Send,
7+
F: Fn(&Request, &Path) -> bool + Sync + Send,
68
{
79
/// Matches if calling the Fn(&Request) with the given request returns true
810
///
911
/// # Examples
1012
///
1113
/// ```
14+
/// # use std::path::Path;
1215
/// # use lang_handler::{Request, rewrite::Condition};
13-
/// let condition = |request: &Request| {
16+
/// # let docroot = std::env::temp_dir();
17+
/// let condition = |request: &Request, _docroot: &Path| {
1418
/// request.url().path().contains("/foo")
1519
/// };
1620
///
@@ -19,9 +23,9 @@ where
1923
/// .build()
2024
/// .expect("request should build");
2125
///
22-
/// assert_eq!(condition.matches(&request), false);
26+
/// assert_eq!(condition.matches(&request, &docroot), false);
2327
/// ```
24-
fn matches(&self, request: &Request) -> bool {
25-
self(request)
28+
fn matches(&self, request: &Request, docroot: &Path) -> bool {
29+
self(request, docroot)
2630
}
2731
}
Lines changed: 48 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,11 @@
1-
use std::path::PathBuf;
1+
use std::path::Path;
22

33
use super::Condition;
44
use super::Request;
55

66
/// Match if request path exists
7-
#[derive(Clone, Debug)]
8-
pub struct ExistenceCondition(PathBuf);
9-
10-
impl ExistenceCondition {
11-
/// Construct an ExistenceCondition to check within a given base directory.
12-
///
13-
/// # Examples
14-
///
15-
/// ```
16-
/// # use lang_handler::rewrite::{Condition, ExistenceCondition};
17-
/// # use lang_handler::Request;
18-
/// let condition = ExistenceCondition::new("/foo/bar");
19-
/// ```
20-
pub fn new<P>(base: P) -> Self
21-
where
22-
P: Into<PathBuf>,
23-
{
24-
Self(base.into())
25-
}
26-
}
7+
#[derive(Clone, Debug, Default)]
8+
pub struct ExistenceCondition;
279

2810
impl Condition for ExistenceCondition {
2911
/// An ExistenceCondition matches a request if the path segment of the
@@ -32,21 +14,35 @@ impl Condition for ExistenceCondition {
3214
/// # Examples
3315
///
3416
/// ```
35-
/// # use lang_handler::rewrite::{Condition, ExistenceCondition};
36-
/// # use lang_handler::Request;
37-
/// let condition = ExistenceCondition::new("/foo/bar");
17+
/// # use lang_handler::{
18+
/// # rewrite::{Condition, ExistenceCondition},
19+
/// # Request,
20+
/// # MockRoot
21+
/// # };
22+
/// #
23+
/// # let docroot = MockRoot::builder()
24+
/// # .file("exists.php", "<?php echo \"Hello, world!\"; ?>")
25+
/// # .build()
26+
/// # .expect("should prepare docroot");
27+
/// let condition = ExistenceCondition;
3828
///
3929
/// let request = Request::builder()
40-
/// .url("http://example.com/index.php")
30+
/// .url("http://example.com/exists.php")
4131
/// .build()
4232
/// .expect("should build request");
4333
///
44-
/// assert_eq!(condition.matches(&request), false);
34+
/// assert!(condition.matches(&request, &docroot));
35+
/// # assert!(!condition.matches(
36+
/// # &request.extend()
37+
/// # .url("http://example.com/does_not_exist.php")
38+
/// # .build()
39+
/// # .expect("should build request"),
40+
/// # &docroot
41+
/// # ));
4542
/// ```
46-
fn matches(&self, request: &Request) -> bool {
43+
fn matches(&self, request: &Request, docroot: &Path) -> bool {
4744
let path = request.url().path();
48-
self
49-
.0
45+
docroot
5046
.join(path.strip_prefix("/").unwrap_or(path))
5147
.canonicalize()
5248
.is_ok()
@@ -55,25 +51,7 @@ impl Condition for ExistenceCondition {
5551

5652
/// Match if request path does not exist
5753
#[derive(Clone, Debug, Default)]
58-
pub struct NonExistenceCondition(PathBuf);
59-
60-
impl NonExistenceCondition {
61-
/// Construct a NonExistenceCondition to check within a given base directory.
62-
///
63-
/// # Examples
64-
///
65-
/// ```
66-
/// # use lang_handler::rewrite::{Condition, NonExistenceCondition};
67-
/// # use lang_handler::Request;
68-
/// let condition = NonExistenceCondition::new("/foo/bar");
69-
/// ```
70-
pub fn new<P>(base: P) -> Self
71-
where
72-
P: Into<PathBuf>,
73-
{
74-
Self(base.into())
75-
}
76-
}
54+
pub struct NonExistenceCondition;
7755

7856
impl Condition for NonExistenceCondition {
7957
/// A NonExistenceCondition matches a request if the path segment of the
@@ -82,60 +60,37 @@ impl Condition for NonExistenceCondition {
8260
/// # Examples
8361
///
8462
/// ```
85-
/// # use lang_handler::rewrite::{Condition, NonExistenceCondition};
86-
/// # use lang_handler::Request;
87-
/// let condition = NonExistenceCondition::new("/foo/bar");
63+
/// # use lang_handler::{
64+
/// # rewrite::{Condition, NonExistenceCondition},
65+
/// # Request,
66+
/// # MockRoot
67+
/// # };
68+
/// #
69+
/// # let docroot = MockRoot::builder()
70+
/// # .file("exists.php", "<?php echo \"Hello, world!\"; ?>")
71+
/// # .build()
72+
/// # .expect("should prepare docroot");
73+
/// let condition = NonExistenceCondition;
8874
///
8975
/// let request = Request::builder()
90-
/// .url("http://example.com/index.php")
76+
/// .url("http://example.com/does_not_exist.php")
9177
/// .build()
9278
/// .expect("should build request");
9379
///
94-
/// assert!(condition.matches(&request));
80+
/// assert!(condition.matches(&request, &docroot));
81+
/// # assert!(!condition.matches(
82+
/// # &request.extend()
83+
/// # .url("http://example.com/exists.php")
84+
/// # .build()
85+
/// # .expect("should build request"),
86+
/// # &docroot
87+
/// # ));
9588
/// ```
96-
fn matches(&self, request: &Request) -> bool {
89+
fn matches(&self, request: &Request, docroot: &Path) -> bool {
9790
let path = request.url().path();
98-
self
99-
.0
91+
docroot
10092
.join(path.strip_prefix("/").unwrap_or(path))
10193
.canonicalize()
10294
.is_err()
10395
}
10496
}
105-
106-
#[cfg(test)]
107-
mod test {
108-
use super::*;
109-
use crate::MockRoot;
110-
111-
#[test]
112-
fn test_existence_condition() {
113-
let docroot = MockRoot::builder()
114-
.file("exists.php", "<?php echo \"Hello, world!\"; ?>")
115-
.build()
116-
.expect("should prepare docroot");
117-
118-
let condition = ExistenceCondition::new(docroot.clone());
119-
120-
let request = Request::builder()
121-
.url("http://example.com/exists.php")
122-
.build()
123-
.expect("request should build");
124-
125-
assert!(condition.matches(&request));
126-
}
127-
128-
#[test]
129-
fn test_non_existence_condition() {
130-
let docroot = MockRoot::builder().build().expect("should prepare docroot");
131-
132-
let condition = NonExistenceCondition::new(docroot.clone());
133-
134-
let request = Request::builder()
135-
.url("http://example.com/does_not_exist.php")
136-
.build()
137-
.expect("request should build");
138-
139-
assert!(condition.matches(&request));
140-
}
141-
}

‎crates/lang_handler/src/rewrite/condition/group.rs‎

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::path::Path;
2+
13
use super::{Condition, Request};
24

35
// Tested via Condition::and(...) and Condition::or(...) doctests
@@ -32,10 +34,34 @@ where
3234
A: Condition + ?Sized,
3335
B: Condition + ?Sized,
3436
{
35-
fn matches(&self, request: &Request) -> bool {
37+
/// Evaluates the condition group against the provided request.
38+
///
39+
/// # Examples
40+
///
41+
/// ```
42+
/// # use std::path::Path;
43+
/// # let docroot = std::env::temp_dir();
44+
/// # use lang_handler::{Request, rewrite::{Condition, ConditionGroup}};
45+
/// let condition = ConditionGroup::or(
46+
/// Box::new(|_req: &Request, _docroot: &Path| true),
47+
/// Box::new(|_req: &Request, _docroot: &Path| false),
48+
/// );
49+
///
50+
/// let request = Request::builder()
51+
/// .url("http://example.com")
52+
/// .build()
53+
/// .expect("should build request");
54+
///
55+
/// assert!(condition.matches(&request, &docroot));
56+
/// ```
57+
fn matches(&self, request: &Request, docroot: &Path) -> bool {
3658
match self {
37-
ConditionGroup::Or(a, b) => a.matches(request) || b.matches(request),
38-
ConditionGroup::And(a, b) => a.matches(request) && b.matches(request),
59+
ConditionGroup::Or(a, b) => {
60+
a.matches(request, docroot) || b.matches(request, docroot)
61+
},
62+
ConditionGroup::And(a, b) => {
63+
a.matches(request, docroot) && b.matches(request, docroot)
64+
},
3965
}
4066
}
4167
}

‎crates/lang_handler/src/rewrite/condition/header.rs‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::fmt::Debug;
1+
use std::{fmt::Debug, path::Path};
22

33
use regex::{Error, Regex};
44

@@ -45,6 +45,7 @@ impl Condition for HeaderCondition {
4545
/// ```
4646
/// # use lang_handler::rewrite::{Condition, HeaderCondition};
4747
/// # use lang_handler::Request;
48+
/// # let docroot = std::env::temp_dir();
4849
/// let condition = HeaderCondition::new("TEST", "^foo$")
4950
/// .expect("should be valid regex");
5051
///
@@ -54,9 +55,9 @@ impl Condition for HeaderCondition {
5455
/// .build()
5556
/// .expect("should build request");
5657
///
57-
/// assert!(condition.matches(&request));
58+
/// assert!(condition.matches(&request, &docroot));
5859
/// ```
59-
fn matches(&self, request: &Request) -> bool {
60+
fn matches(&self, request: &Request, _docroot: &Path) -> bool {
6061
request
6162
.headers()
6263
.get_line(&self.name)

‎crates/lang_handler/src/rewrite/condition/mod.rs‎

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,25 @@ mod closure;
22
mod existence;
33
mod group;
44
mod header;
5+
mod method;
56
mod path;
67

8+
use std::path::Path;
9+
710
use crate::Request;
811

912
pub use existence::{ExistenceCondition, NonExistenceCondition};
1013
pub use group::ConditionGroup;
1114
pub use header::HeaderCondition;
15+
pub use method::MethodCondition;
1216
pub use path::PathCondition;
1317

1418
/// A Condition is used to match against request state before deciding to apply
1519
/// a given Rewrite or set of Rewrites.
1620
pub trait Condition: Sync + Send {
1721
/// A Condition must implement a `matches(request) -> bool` method which
1822
/// receives a request object to determine if the condition is met.
19-
fn matches(&self, request: &Request) -> bool;
23+
fn matches(&self, request: &Request, docroot: &Path) -> bool;
2024
}
2125

2226
impl<T: ?Sized> ConditionExt for T where T: Condition {}
@@ -31,6 +35,7 @@ pub trait ConditionExt: Condition {
3135
/// # Request,
3236
/// # rewrite::{Condition, ConditionExt, PathCondition, HeaderCondition}
3337
/// # };
38+
/// # let docroot = std::env::temp_dir();
3439
/// let path = PathCondition::new("^/index.php$")
3540
/// .expect("should be valid regex");
3641
///
@@ -45,7 +50,7 @@ pub trait ConditionExt: Condition {
4550
/// .build()
4651
/// .expect("should build request");
4752
///
48-
/// assert!(condition.matches(&request));
53+
/// assert!(condition.matches(&request, &docroot));
4954
/// #
5055
/// # // SHould _not_ match if either condition does not match
5156
/// # let only_header = Request::builder()
@@ -54,14 +59,14 @@ pub trait ConditionExt: Condition {
5459
/// # .build()
5560
/// # .expect("request should build");
5661
/// #
57-
/// # assert!(!condition.matches(&only_header));
62+
/// # assert!(!condition.matches(&only_header, &docroot));
5863
/// #
5964
/// # let only_url = Request::builder()
6065
/// # .url("http://example.com/index.php")
6166
/// # .build()
6267
/// # .expect("request should build");
6368
/// #
64-
/// # assert!(!condition.matches(&only_url));
69+
/// # assert!(!condition.matches(&only_url, &docroot));
6570
/// ```
6671
fn and<C>(self: Box<Self>, other: Box<C>) -> Box<ConditionGroup<Self, C>>
6772
where
@@ -79,6 +84,7 @@ pub trait ConditionExt: Condition {
7984
/// # Request,
8085
/// # rewrite::{Condition, ConditionExt, PathCondition, HeaderCondition}
8186
/// # };
87+
/// # let docroot = std::env::temp_dir();
8288
/// let path = PathCondition::new("^/index.php$")
8389
/// .expect("should be valid regex");
8490
///
@@ -92,7 +98,7 @@ pub trait ConditionExt: Condition {
9298
/// .build()
9399
/// .expect("should build request");
94100
///
95-
/// assert!(condition.matches(&request));
101+
/// assert!(condition.matches(&request, &docroot));
96102
/// #
97103
/// # // Should match if one condition does not
98104
/// # let only_header = Request::builder()
@@ -101,14 +107,14 @@ pub trait ConditionExt: Condition {
101107
/// # .build()
102108
/// # .expect("request should build");
103109
/// #
104-
/// # assert!(condition.matches(&only_header));
110+
/// # assert!(condition.matches(&only_header, &docroot));
105111
/// #
106112
/// # let only_url = Request::builder()
107113
/// # .url("http://example.com/index.php")
108114
/// # .build()
109115
/// # .expect("request should build");
110116
/// #
111-
/// # assert!(condition.matches(&only_url));
117+
/// # assert!(condition.matches(&only_url, &docroot));
112118
/// ```
113119
fn or<C>(self: Box<Self>, other: Box<C>) -> Box<ConditionGroup<Self, C>>
114120
where

‎crates/lang_handler/src/rewrite/condition/path.rs‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::fmt::Debug;
1+
use std::{path::Path, fmt::Debug};
22

33
use regex::{Error, Regex};
44

@@ -32,6 +32,7 @@ impl Condition for PathCondition {
3232
/// ```
3333
/// # use lang_handler::rewrite::{Condition, PathCondition};
3434
/// # use lang_handler::Request;
35+
/// # let docroot = std::env::temp_dir();
3536
/// let condition = PathCondition::new("^/index.php$")
3637
/// .expect("should be valid regex");
3738
///
@@ -40,9 +41,9 @@ impl Condition for PathCondition {
4041
/// .build()
4142
/// .expect("should build request");
4243
///
43-
/// assert!(condition.matches(&request));
44+
/// assert!(condition.matches(&request, &docroot));
4445
/// ```
45-
fn matches(&self, request: &Request) -> bool {
46+
fn matches(&self, request: &Request, _docroot: &Path) -> bool {
4647
self.pattern.is_match(request.url().path())
4748
}
4849
}

‎crates/lang_handler/src/rewrite/conditional_rewriter.rs‎

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
use std::path::Path;
2+
13
use crate::{
24
rewrite::{Condition, Rewriter},
35
Request,
6+
RequestBuilderException
47
};
58

69
// Tested via Rewriter::when(...) doc-test
@@ -25,8 +28,8 @@ where
2528
R: Rewriter + ?Sized,
2629
C: Condition + ?Sized,
2730
{
28-
fn matches(&self, request: &Request) -> bool {
29-
self.1.matches(request)
31+
fn matches(&self, request: &Request, docroot: &Path) -> bool {
32+
self.1.matches(request, docroot)
3033
}
3134
}
3235

@@ -35,11 +38,12 @@ where
3538
R: Rewriter + ?Sized,
3639
C: Condition + ?Sized,
3740
{
38-
fn rewrite(&self, request: Request) -> Request {
39-
self
40-
.matches(&request)
41-
.then(|| self.0.rewrite(request.clone()))
42-
.or(Some(request))
43-
.expect("should produce a request")
41+
fn rewrite(&self, request: Request, docroot: &Path)
42+
-> Result<Request, RequestBuilderException> {
43+
if !self.matches(&request, docroot) {
44+
return Ok(request);
45+
}
46+
47+
self.0.rewrite(request, docroot)
4448
}
4549
}

‎crates/lang_handler/src/rewrite/mod.rs‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
//! may be dispatched to any thread, these functions must be `Send + Sync`.
2121
//!
2222
//! ```
23+
//! # use lang_handler::{Request, rewrite::Condition};
2324
//! let condition = |request: &Request| -> bool {
2425
//! request.url().path().starts_with("/foo")
2526
//! };
@@ -42,7 +43,8 @@
4243
//! be dispatched to any thread, these functions must be `Send + Sync`
4344
//!
4445
//! ```
45-
//! let rewriter = |request: Request| -> Request {
46+
//! # use lang_handler::{Request, RequestBuilderException, rewrite::Rewriter};
47+
//! let rewriter = |request: Request| -> Result<Request, RequestBuilderException> {
4648
//! request.extend()
4749
//! .url("http://example.com/rewritten")
4850
//! .build()
@@ -65,8 +67,16 @@
6567
//! achieve some quite complex rewriting logic.
6668
//!
6769
//! ```rust
68-
//! # use lang_handler::rewrite::{Condition, Rewriter, PathCondition, PathRewriter};
69-
//!
70+
//! # use lang_handler::rewrite::{
71+
//! # Condition,
72+
//! # ConditionExt,
73+
//! # HeaderCondition,
74+
//! # PathCondition,
75+
//! # Rewriter,
76+
//! # RewriterExt,
77+
//! # PathRewriter
78+
//! # };
79+
//! #
7080
//! let admin = {
7181
//! let is_admin_path = PathCondition::new("^/admin")
7282
//! .expect("regex is valid");
Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,37 @@
1-
use super::{Request, Rewriter};
1+
use std::path::Path;
2+
3+
use super::{Request, RequestBuilderException, Rewriter};
24

35
impl<F> Rewriter for F
46
where
5-
F: Fn(Request) -> Request + Sync + Send,
7+
F: Fn(Request, &Path) -> Result<Request, RequestBuilderException> + Sync + Send,
68
{
79
/// Rewrites the request by calling the Fn(&Request) with the given request
810
///
911
/// # Examples
1012
///
1113
/// ```
14+
/// # use std::path::Path;
1215
/// # use lang_handler::{Request, rewrite::Rewriter};
13-
/// let rewriter = |request: Request| {
16+
/// # let docroot = std::env::temp_dir();
17+
/// let rewriter = |request: Request, docroot: &Path| {
1418
/// request.extend()
1519
/// .url("http://example.com/foo/bar")
1620
/// .build()
17-
/// .expect("should build new request")
1821
/// };
1922
///
2023
/// let request = Request::builder()
2124
/// .url("http://example.com/index.php")
2225
/// .build()
2326
/// .expect("request should build");
2427
///
25-
/// assert_eq!(
26-
/// rewriter.rewrite(request).url().path(),
27-
/// "/foo/bar".to_string()
28-
/// );
28+
/// let new_request = rewriter.rewrite(request, &docroot)
29+
/// .expect("rewriting should succeed");
30+
///
31+
/// assert_eq!(new_request.url().path(), "/foo/bar".to_string());
2932
/// ```
30-
fn rewrite(&self, request: Request) -> Request {
31-
self(request)
33+
fn rewrite(&self, request: Request, docroot: &Path)
34+
-> Result<Request, RequestBuilderException> {
35+
self(request, docroot)
3236
}
3337
}

‎crates/lang_handler/src/rewrite/rewriter/header.rs‎

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
use std::path::Path;
2+
13
use regex::{Error, Regex};
24

3-
use super::{Request, Rewriter};
5+
use super::{Request, RequestBuilderException, Rewriter};
46

57
/// Rewrite a request header using a given pattern and replacement.
68
pub struct HeaderRewriter {
@@ -47,6 +49,7 @@ impl Rewriter for HeaderRewriter {
4749
/// ```
4850
/// # use lang_handler::rewrite::{Rewriter, HeaderRewriter};
4951
/// # use lang_handler::Request;
52+
/// # let docroot = std::env::temp_dir();
5053
/// let rewriter = HeaderRewriter::new("TEST", "(foo)", "${1}bar")
5154
/// .expect("should be valid regex");
5255
///
@@ -56,25 +59,28 @@ impl Rewriter for HeaderRewriter {
5659
/// .build()
5760
/// .expect("should build request");
5861
///
62+
/// let new_request = rewriter.rewrite(request, &docroot)
63+
/// .expect("should rewrite request");
64+
///
5965
/// assert_eq!(
60-
/// rewriter.rewrite(request).headers().get("TEST"),
66+
/// new_request.headers().get("TEST"),
6167
/// Some("foobar".to_string())
6268
/// );
6369
/// ```
64-
fn rewrite(&self, request: Request) -> Request {
70+
fn rewrite(&self, request: Request, _docroot: &Path)
71+
-> Result<Request, RequestBuilderException> {
6572
let HeaderRewriter {
6673
name,
6774
pattern,
6875
replacement,
6976
} = self;
7077

7178
match request.headers().get(name) {
72-
None => request,
79+
None => Ok(request),
7380
Some(value) => request
7481
.extend()
7582
.header(name, pattern.replace(&value, replacement.clone()))
76-
.build()
77-
.unwrap_or(request),
83+
.build(),
7884
}
7985
}
8086
}

‎crates/lang_handler/src/rewrite/rewriter/mod.rs‎

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
1+
use std::path::Path;
2+
13
use crate::{
24
rewrite::{Condition, ConditionalRewriter},
35
Request,
6+
RequestBuilderException,
47
};
58

69
mod closure;
710
mod header;
11+
mod href;
12+
mod method;
813
mod path;
914
mod sequence;
1015

1116
pub use header::HeaderRewriter;
17+
pub use href::HrefRewriter;
18+
pub use method::MethodRewriter;
1219
pub use path::PathRewriter;
1320
pub use sequence::RewriterSequence;
1421

1522
/// A Rewriter simply applies its rewrite function to produce a possibly new
1623
/// request object.
1724
pub trait Rewriter: Sync + Send {
18-
fn rewrite(&self, request: Request) -> Request;
25+
fn rewrite(&self, request: Request, docroot: &Path)
26+
-> Result<Request, RequestBuilderException>;
1927
}
2028

2129
impl<T: ?Sized> RewriterExt for T where T: Rewriter {}
@@ -30,6 +38,7 @@ pub trait RewriterExt: Rewriter {
3038
/// # Request,
3139
/// # rewrite::{Rewriter, RewriterExt, PathCondition, PathRewriter}
3240
/// # };
41+
/// # let docroot = std::env::temp_dir();
3342
/// let rewriter = PathRewriter::new("^(/index\\.php)$", "/foo$1")
3443
/// .expect("should be valid regex");
3544
///
@@ -43,10 +52,10 @@ pub trait RewriterExt: Rewriter {
4352
/// .build()
4453
/// .expect("should build request");
4554
///
46-
/// assert_eq!(
47-
/// conditional_rewriter.rewrite(request).url().path(),
48-
/// "/foo/index.php".to_string()
49-
/// );
55+
/// let new_request = conditional_rewriter.rewrite(request, &docroot)
56+
/// .expect("should rewrite request");
57+
///
58+
/// assert_eq!(new_request.url().path(), "/foo/index.php".to_string());
5059
/// ```
5160
fn when<C>(self: Box<Self>, condition: Box<C>) -> Box<ConditionalRewriter<Self, C>>
5261
where
@@ -64,6 +73,7 @@ pub trait RewriterExt: Rewriter {
6473
/// # Request,
6574
/// # rewrite::{Rewriter, RewriterExt, PathRewriter, HeaderRewriter}
6675
/// # };
76+
/// # let docroot = std::env::temp_dir();
6777
/// let first = PathRewriter::new("^(/index.php)$", "/foo$1")
6878
/// .expect("should be valid regex");
6979
///
@@ -78,10 +88,10 @@ pub trait RewriterExt: Rewriter {
7888
/// .build()
7989
/// .expect("should build request");
8090
///
81-
/// assert_eq!(
82-
/// sequence.rewrite(request).url().path(),
83-
/// "/foo/bar.php".to_string()
84-
/// );
91+
/// let new_request = sequence.rewrite(request, &docroot)
92+
/// .expect("should rewrite request");
93+
///
94+
/// assert_eq!(new_request.url().path(), "/foo/bar.php".to_string());
8595
/// ```
8696
fn then<R>(self: Box<Self>, rewriter: Box<R>) -> Box<RewriterSequence<Self, R>>
8797
where

‎crates/lang_handler/src/rewrite/rewriter/path.rs‎

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
use std::path::Path;
2+
13
use regex::{Error, Regex};
24

3-
use super::{Request, Rewriter};
5+
use super::{Request, RequestBuilderException, Rewriter};
46

57
/// Rewrite a request path using a given pattern and replacement.
68
pub struct PathRewriter {
@@ -42,6 +44,7 @@ impl Rewriter for PathRewriter {
4244
/// ```
4345
/// # use lang_handler::rewrite::{Rewriter, PathRewriter};
4446
/// # use lang_handler::Request;
47+
/// # let docroot = std::env::temp_dir();
4548
/// let rewriter = PathRewriter::new("^(/foo)$", "/index.php")
4649
/// .expect("should be valid regex");
4750
///
@@ -50,23 +53,28 @@ impl Rewriter for PathRewriter {
5053
/// .build()
5154
/// .expect("should build request");
5255
///
53-
/// assert_eq!(
54-
/// rewriter.rewrite(request).url().path(),
55-
/// "/index.php".to_string()
56-
/// );
56+
/// let new_request = rewriter.rewrite(request, &docroot)
57+
/// .expect("should rewrite request");
58+
///
59+
/// assert_eq!(new_request.url().path(), "/index.php".to_string());
5760
/// ```
58-
fn rewrite(&self, request: Request) -> Request {
59-
let url = request.url();
61+
fn rewrite(&self, request: Request, _docroot: &Path)
62+
-> Result<Request, RequestBuilderException> {
63+
let PathRewriter { pattern, replacement, } = self;
6064

61-
let PathRewriter {
62-
pattern,
63-
replacement,
64-
} = self;
65-
let path = pattern.replace(url.path(), replacement.clone());
65+
let input = request.url().path();
66+
let output = pattern.replace(input, replacement.clone());
67+
68+
// No change, return original request
69+
if input == output {
70+
return Ok(request);
71+
}
6672

67-
let mut copy = url.clone();
68-
copy.set_path(path.as_ref());
73+
let mut copy = request.url().clone();
74+
copy.set_path(output.as_ref());
6975

70-
request.extend().url(copy).build().unwrap_or(request)
76+
request.extend()
77+
.url(copy)
78+
.build()
7179
}
7280
}

‎crates/lang_handler/src/rewrite/rewriter/sequence.rs‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use super::{Request, Rewriter};
1+
use std::path::Path;
2+
3+
use super::{Request, RequestBuilderException, Rewriter};
24

35
// Tested via Rewriter::then(...) doc-test
46

@@ -23,8 +25,9 @@ where
2325
A: Rewriter + ?Sized,
2426
B: Rewriter + ?Sized,
2527
{
26-
fn rewrite(&self, request: Request) -> Request {
27-
let request = self.0.rewrite(request);
28-
self.1.rewrite(request)
28+
fn rewrite(&self, request: Request, docroot: &Path)
29+
-> Result<Request, RequestBuilderException> {
30+
let request = self.0.rewrite(request, docroot)?;
31+
self.1.rewrite(request, docroot)
2932
}
3033
}

‎crates/php/src/embed.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,8 @@ impl Handler for Embed {
189189
// Apply request rewriting rules
190190
let mut request = request.clone();
191191
if let Some(rewriter) = &self.rewriter {
192-
request = rewriter.rewrite(request)
192+
request = rewriter.rewrite(request, &docroot)
193+
.map_err(EmbedRequestError::RequestRewriteError)?;
193194
}
194195

195196
// Initialize the SAPI module

‎crates/php/src/exception.rs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use lang_handler::RequestBuilderException;
2+
13
/// Set of exceptions which may be produced by php::Embed
24
#[derive(Debug, PartialEq, Eq, Hash)]
35
pub enum EmbedStartError {
@@ -37,6 +39,7 @@ pub enum EmbedRequestError {
3739
FailedToDetermineContentType,
3840
FailedToSetServerVar(String),
3941
FailedToSetRequestInfo(String),
42+
RequestRewriteError(RequestBuilderException),
4043
}
4144

4245
impl std::fmt::Display for EmbedRequestError {
@@ -68,6 +71,9 @@ impl std::fmt::Display for EmbedRequestError {
6871
}
6972
EmbedRequestError::FailedToSetRequestInfo(name) => {
7073
write!(f, "Failed to set request info: \"{}\"", name)
74+
},
75+
EmbedRequestError::RequestRewriteError(e) => {
76+
write!(f, "Request rewrite error: {}", e)
7177
}
7278
}
7379
}

‎crates/php/src/lib.rs‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,18 @@ mod scopes;
99
mod strings;
1010
mod test;
1111

12-
pub use lang_handler::{rewrite, Handler, Header, Headers, Request, RequestBuilder, Response, Url};
12+
pub use lang_handler::{
13+
rewrite,
14+
Handler,
15+
Header,
16+
Headers,
17+
Request,
18+
RequestBuilder,
19+
RequestBuilderException,
20+
Response,
21+
ResponseBuilder,
22+
Url,
23+
};
1324

1425
pub use embed::Embed;
1526
pub use exception::{EmbedRequestError, EmbedStartError};

‎crates/php_node/src/rewriter.rs‎

Lines changed: 108 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
1-
use std::str::FromStr;
1+
use std::{path::Path, str::FromStr};
22

33
// use napi::bindgen_prelude::*;
44
use napi::{Error, Result};
55

66
use php::{
77
rewrite::{
8-
Condition, ConditionExt, HeaderCondition, HeaderRewriter, PathCondition, PathRewriter,
9-
Rewriter, RewriterExt,
8+
Condition,
9+
ConditionExt,
10+
ExistenceCondition,
11+
HeaderCondition,
12+
MethodCondition,
13+
NonExistenceCondition,
14+
PathCondition,
15+
HeaderRewriter,
16+
HrefRewriter,
17+
MethodRewriter,
18+
PathRewriter,
19+
Rewriter,
20+
RewriterExt,
1021
},
1122
Request,
23+
RequestBuilderException,
1224
};
1325

1426
use crate::PhpRequest;
@@ -18,28 +30,38 @@ use crate::PhpRequest;
1830
//
1931

2032
#[napi(object)]
21-
#[derive(Debug, Default)]
33+
#[derive(Clone, Debug, Default)]
2234
pub struct PhpRewriteCondOptions {
2335
#[napi(js_name = "type")]
2436
pub cond_type: String,
25-
pub args: Vec<String>,
37+
pub args: Option<Vec<String>>,
2638
}
2739

2840
pub enum PhpRewriteCond {
29-
Path(String),
41+
Exists,
3042
Header(String, String),
43+
Method(String),
44+
NotExists,
45+
Path(String),
3146
}
3247

3348
impl Condition for PhpRewriteCond {
34-
fn matches(&self, request: &php::Request) -> bool {
49+
fn matches(&self, request: &php::Request, docroot: &Path) -> bool {
3550
let condition: Box<dyn Condition> = match self {
36-
PhpRewriteCond::Path(pattern) => PathCondition::new(pattern.as_str()).unwrap(),
51+
PhpRewriteCond::Exists => Box::new(ExistenceCondition),
3752
PhpRewriteCond::Header(name, pattern) => {
3853
HeaderCondition::new(name.as_str(), pattern.as_str()).unwrap()
39-
}
54+
},
55+
PhpRewriteCond::Method(pattern) => {
56+
MethodCondition::new(pattern.as_str()).unwrap()
57+
},
58+
PhpRewriteCond::NotExists => Box::new(NonExistenceCondition),
59+
PhpRewriteCond::Path(pattern) => {
60+
PathCondition::new(pattern.as_str()).unwrap()
61+
},
4062
};
4163

42-
condition.matches(request)
64+
condition.matches(request, docroot)
4365
}
4466
}
4567

@@ -49,23 +71,38 @@ impl TryFrom<&PhpRewriteCondOptions> for Box<PhpRewriteCond> {
4971
fn try_from(value: &PhpRewriteCondOptions) -> std::result::Result<Self, Self::Error> {
5072
let PhpRewriteCondOptions { cond_type, args } = value;
5173
let cond_type = cond_type.to_lowercase();
74+
let args = args.to_owned().unwrap_or(vec![]);
5275
match cond_type.as_str() {
53-
"path" => match args.len() {
54-
1 => Ok(Box::new(PhpRewriteCond::Path(args[0].to_owned()))),
55-
_ => Err(Error::from_reason("Wrong number of parameters")),
56-
},
76+
"exists" => if args.is_empty() {
77+
Ok(Box::new(PhpRewriteCond::Exists))
78+
} else {
79+
Err(Error::from_reason("Wrong number of parameters"))
80+
}
5781
"header" => match args.len() {
5882
2 => {
5983
let name = args[0].to_owned();
6084
let pattern = args[1].to_owned();
6185
Ok(Box::new(PhpRewriteCond::Header(name, pattern)))
6286
}
6387
_ => Err(Error::from_reason("Wrong number of parameters")),
64-
},
88+
}
89+
"method" => match args.len() {
90+
1 => Ok(Box::new(PhpRewriteCond::Method(args[0].to_owned()))),
91+
_ => Err(Error::from_reason("Wrong number of parameters")),
92+
}
93+
"not_exists" | "not-exists" => if args.is_empty() {
94+
Ok(Box::new(PhpRewriteCond::NotExists))
95+
} else {
96+
Err(Error::from_reason("Wrong number of parameters"))
97+
}
98+
"path" => match args.len() {
99+
1 => Ok(Box::new(PhpRewriteCond::Path(args[0].to_owned()))),
100+
_ => Err(Error::from_reason("Wrong number of parameters")),
101+
}
65102
_ => Err(Error::from_reason(format!(
66103
"Unknown condition type: {}",
67104
cond_type
68-
))),
105+
)))
69106
}
70107
}
71108
}
@@ -75,30 +112,39 @@ impl TryFrom<&PhpRewriteCondOptions> for Box<PhpRewriteCond> {
75112
//
76113

77114
#[napi(object)]
78-
#[derive(Debug, Default)]
115+
#[derive(Clone, Debug, Default)]
79116
pub struct PhpRewriterOptions {
80117
#[napi(js_name = "type")]
81118
pub rewriter_type: String,
82119
pub args: Vec<String>,
83120
}
84121

85122
pub enum PhpRewriterType {
86-
Path(String, String),
87123
Header(String, String, String),
124+
Href(String, String),
125+
Method(String, String),
126+
Path(String, String),
88127
}
89128

90129
impl Rewriter for PhpRewriterType {
91-
fn rewrite(&self, request: Request) -> Request {
130+
fn rewrite(&self, request: Request, docroot: &Path)
131+
-> std::result::Result<Request, RequestBuilderException> {
92132
let rewriter: Box<dyn Rewriter> = match self {
93133
PhpRewriterType::Path(pattern, replacement) => {
94134
PathRewriter::new(pattern.as_str(), replacement.as_str()).unwrap()
95135
}
136+
PhpRewriterType::Href(pattern, replacement) => {
137+
HrefRewriter::new(pattern.as_str(), replacement.as_str()).unwrap()
138+
}
139+
PhpRewriterType::Method(pattern, replacement) => {
140+
MethodRewriter::new(pattern.as_str(), replacement.as_str()).unwrap()
141+
}
96142
PhpRewriterType::Header(name, pattern, replacement) => {
97143
HeaderRewriter::new(name.as_str(), pattern.as_str(), replacement.as_str()).unwrap()
98144
}
99145
};
100146

101-
rewriter.rewrite(request)
147+
rewriter.rewrite(request, docroot)
102148
}
103149
}
104150

@@ -112,14 +158,6 @@ impl TryFrom<&PhpRewriterOptions> for Box<PhpRewriterType> {
112158
} = value;
113159
let rewriter_type = rewriter_type.to_lowercase();
114160
match rewriter_type.as_str() {
115-
"path" => match args.len() {
116-
2 => {
117-
let pattern = args[0].to_owned();
118-
let replacement = args[1].to_owned();
119-
Ok(Box::new(PhpRewriterType::Path(pattern, replacement)))
120-
}
121-
_ => Err(Error::from_reason("Wrong number of parameters")),
122-
},
123161
"header" => match args.len() {
124162
3 => {
125163
let name = args[0].to_owned();
@@ -133,6 +171,30 @@ impl TryFrom<&PhpRewriterOptions> for Box<PhpRewriterType> {
133171
}
134172
_ => Err(Error::from_reason("Wrong number of parameters")),
135173
},
174+
"href" => match args.len() {
175+
2 => {
176+
let pattern = args[0].to_owned();
177+
let replacement = args[1].to_owned();
178+
Ok(Box::new(PhpRewriterType::Href(pattern, replacement)))
179+
}
180+
_ => Err(Error::from_reason("Wrong number of parameters")),
181+
},
182+
"method" => match args.len() {
183+
2 => {
184+
let pattern = args[0].to_owned();
185+
let replacement = args[1].to_owned();
186+
Ok(Box::new(PhpRewriterType::Method(pattern, replacement)))
187+
}
188+
_ => Err(Error::from_reason("Wrong number of parameters")),
189+
},
190+
"path" => match args.len() {
191+
2 => {
192+
let pattern = args[0].to_owned();
193+
let replacement = args[1].to_owned();
194+
Ok(Box::new(PhpRewriterType::Path(pattern, replacement)))
195+
}
196+
_ => Err(Error::from_reason("Wrong number of parameters")),
197+
},
136198
_ => Err(Error::from_reason(format!(
137199
"Unknown rewriter type: {}",
138200
rewriter_type
@@ -165,37 +227,34 @@ impl FromStr for OperationType {
165227
}
166228

167229
#[napi(object)]
168-
#[derive(Debug, Default)]
230+
#[derive(Clone, Debug, Default)]
169231
pub struct PhpConditionalRewriterOptions {
170232
pub operation: Option<String>,
171-
pub conditions: Vec<PhpRewriteCondOptions>,
233+
pub conditions: Option<Vec<PhpRewriteCondOptions>>,
172234
pub rewriters: Vec<PhpRewriterOptions>,
173235
}
174236

175237
pub struct PhpConditionalRewriter(Box<dyn Rewriter>);
176238

177239
impl Rewriter for PhpConditionalRewriter {
178-
fn rewrite(&self, request: Request) -> Request {
179-
self.0.rewrite(request)
240+
fn rewrite(&self, request: Request, docroot: &Path)
241+
-> std::result::Result<Request, RequestBuilderException> {
242+
self.0.rewrite(request, docroot)
180243
}
181244
}
182245

183246
impl TryFrom<&PhpConditionalRewriterOptions> for Box<PhpConditionalRewriter> {
184247
type Error = Error;
185248

186249
fn try_from(value: &PhpConditionalRewriterOptions) -> std::result::Result<Self, Self::Error> {
187-
let PhpConditionalRewriterOptions {
188-
operation,
189-
rewriters,
190-
conditions,
191-
} = value;
250+
let value = value.clone();
192251

193-
let operation = operation
252+
let operation = value.operation
194253
.clone()
195254
.unwrap_or("and".into())
196255
.parse::<OperationType>()?;
197256

198-
let rewriter = rewriters
257+
let rewriter = value.rewriters
199258
.iter()
200259
.try_fold(None::<Box<dyn Rewriter>>, |state, next| {
201260
let converted: std::result::Result<Box<PhpRewriterType>, Error> = next.try_into();
@@ -208,7 +267,8 @@ impl TryFrom<&PhpConditionalRewriterOptions> for Box<PhpConditionalRewriter> {
208267
})
209268
})?;
210269

211-
let condition = conditions
270+
let condition = value.conditions
271+
.unwrap_or_default()
212272
.iter()
213273
.try_fold(None::<Box<dyn Condition>>, |state, next| {
214274
let converted: std::result::Result<Box<PhpRewriteCond>, Error> = next.try_into();
@@ -249,10 +309,16 @@ impl PhpRewriter {
249309
}
250310

251311
#[napi]
252-
pub fn rewrite(&self, request: &PhpRequest) -> Result<PhpRequest> {
312+
pub fn rewrite(&self, request: &PhpRequest, docroot: String) -> Result<PhpRequest> {
253313
let rewriter = self.into_rewriter()?;
314+
let docroot = Path::new(&docroot);
254315
Ok(PhpRequest {
255-
request: rewriter.rewrite(request.request.to_owned()),
316+
request: rewriter.rewrite(request.request.to_owned(), docroot).map_err(|err| {
317+
Error::from_reason(format!(
318+
"Failed to rewrite request: {}",
319+
err.to_string()
320+
))
321+
})?,
256322
})
257323
}
258324

‎crates/php_node/src/runtime.rs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ pub struct PhpOptions {
1818
/// Throw request errors
1919
pub throw_request_errors: Option<bool>,
2020
/// Request rewriter
21-
pub rewrite: Option<Reference<PhpRewriter>>,
21+
pub rewriter: Option<Reference<PhpRewriter>>,
2222
}
2323

2424
/// A PHP instance.
@@ -62,7 +62,7 @@ impl PhpRuntime {
6262
docroot,
6363
argv,
6464
throw_request_errors,
65-
rewrite,
65+
rewriter,
6666
} = options.unwrap_or_default();
6767

6868
let docroot = docroot
@@ -73,7 +73,7 @@ impl PhpRuntime {
7373
})
7474
.map_err(|_| Error::from_reason("Could not determine docroot"))?;
7575

76-
let rewriter = if let Some(found) = rewrite {
76+
let rewriter = if let Some(found) = rewriter {
7777
Some(found.into_rewriter()?)
7878
} else {
7979
None

‎index.d.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,15 @@ export interface PhpResponseOptions {
4949
}
5050
export interface PhpRewriteCondOptions {
5151
type: string
52-
args: Array<string>
52+
args?: Array<string>
5353
}
5454
export interface PhpRewriterOptions {
5555
type: string
5656
args: Array<string>
5757
}
5858
export interface PhpConditionalRewriterOptions {
5959
operation?: string
60-
conditions: Array<PhpRewriteCondOptions>
60+
conditions?: Array<PhpRewriteCondOptions>
6161
rewriters: Array<PhpRewriterOptions>
6262
}
6363
/** Options for creating a new PHP instance. */
@@ -69,7 +69,7 @@ export interface PhpOptions {
6969
/** Throw request errors */
7070
throwRequestErrors?: boolean
7171
/** Request rewriter */
72-
rewrite?: Rewriter
72+
rewriter?: Rewriter
7373
}
7474
export type PhpHeaders = Headers
7575
/**
@@ -480,7 +480,7 @@ export declare class Response {
480480
export type PhpRewriter = Rewriter
481481
export declare class Rewriter {
482482
constructor(options: Array<PhpConditionalRewriterOptions>)
483-
rewrite(request: Request): Request
483+
rewrite(request: Request, docroot: string): Request
484484
}
485485
export type PhpRuntime = Php
486486
/**

0 commit comments

Comments
 (0)
Please sign in to comment.