From fc517b343d5d09d17e609a7410052267ffd7d52e Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Wed, 27 Nov 2024 21:46:11 +0000 Subject: [PATCH 1/5] new crate for guarding requests --- Cargo.lock | 15 ++++- crates/bsnext_core/Cargo.toml | 1 + crates/bsnext_core/src/handlers/proxy.rs | 2 +- crates/bsnext_guards/Cargo.toml | 11 ++++ crates/bsnext_guards/src/lib.rs | 12 ++++ .../src/path_matcher.rs | 57 +------------------ crates/bsnext_guards/src/route_guard.rs | 7 +++ crates/bsnext_html/src/html_writer.rs | 4 +- crates/bsnext_html/src/lib.rs | 6 +- crates/bsnext_html/tests/html_playground.rs | 2 +- crates/bsnext_input/Cargo.toml | 1 + crates/bsnext_input/src/lib.rs | 6 +- crates/bsnext_input/src/playground.rs | 5 +- crates/bsnext_input/src/server_config.rs | 2 +- crates/bsnext_md/src/md_writer.rs | 2 +- crates/bsnext_resp/Cargo.toml | 2 +- crates/bsnext_resp/src/builtin_strings.rs | 5 +- crates/bsnext_resp/src/connector.rs | 5 +- crates/bsnext_resp/src/debug.rs | 5 +- crates/bsnext_resp/src/inject_addition.rs | 5 +- crates/bsnext_resp/src/inject_opt_test/mod.rs | 55 ++++++++++++++++++ crates/bsnext_resp/src/inject_opts.rs | 15 ++--- crates/bsnext_resp/src/inject_replacement.rs | 5 +- crates/bsnext_resp/src/injector_guard.rs | 11 +--- crates/bsnext_resp/src/lib.rs | 4 +- 25 files changed, 142 insertions(+), 103 deletions(-) create mode 100644 crates/bsnext_guards/Cargo.toml create mode 100644 crates/bsnext_guards/src/lib.rs rename crates/{bsnext_resp => bsnext_guards}/src/path_matcher.rs (59%) create mode 100644 crates/bsnext_guards/src/route_guard.rs diff --git a/Cargo.lock b/Cargo.lock index af16a3d..db76d94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -485,6 +485,7 @@ dependencies = [ "bsnext_client", "bsnext_dto", "bsnext_fs", + "bsnext_guards", "bsnext_input", "bsnext_resp", "bsnext_utils", @@ -549,6 +550,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "bsnext_guards" +version = "0.1.0" +dependencies = [ + "axum", + "http", + "serde", + "tracing", + "urlpattern", +] + [[package]] name = "bsnext_html" version = "0.2.3" @@ -566,6 +578,7 @@ name = "bsnext_input" version = "0.2.3" dependencies = [ "anyhow", + "bsnext_guards", "bsnext_resp", "bsnext_tracing", "clap", @@ -623,6 +636,7 @@ version = "0.2.3" dependencies = [ "anyhow", "axum", + "bsnext_guards", "bytes", "http", "http-body-util", @@ -631,7 +645,6 @@ dependencies = [ "tokio", "tower 0.4.13", "tracing", - "urlpattern", ] [[package]] diff --git a/crates/bsnext_core/Cargo.toml b/crates/bsnext_core/Cargo.toml index 7dc1076..39b0474 100644 --- a/crates/bsnext_core/Cargo.toml +++ b/crates/bsnext_core/Cargo.toml @@ -14,6 +14,7 @@ bsnext_fs = { path = "../bsnext_fs" } bsnext_resp = { path = "../bsnext_resp" } bsnext_client = { path = "../bsnext_client" } bsnext_dto = { path = "../bsnext_dto" } +bsnext_guards = { path = "../bsnext_guards" } axum-server = { version = "0.6.0", features = ["tls-rustls"] } axum-extra = { version = "0.9.3", features = ["typed-header"] } diff --git a/crates/bsnext_core/src/handlers/proxy.rs b/crates/bsnext_core/src/handlers/proxy.rs index 4eaf9fd..b83a93e 100644 --- a/crates/bsnext_core/src/handlers/proxy.rs +++ b/crates/bsnext_core/src/handlers/proxy.rs @@ -5,7 +5,7 @@ use axum::handler::Handler; use axum::response::{IntoResponse, Response}; use axum::routing::any; use axum::Extension; -use bsnext_resp::injector_guard::InjectorGuard; +use bsnext_guards::route_guard::RouteGuard; use bsnext_resp::InjectHandling; use http::{HeaderValue, StatusCode, Uri}; use hyper_tls::HttpsConnector; diff --git a/crates/bsnext_guards/Cargo.toml b/crates/bsnext_guards/Cargo.toml new file mode 100644 index 0000000..76d403b --- /dev/null +++ b/crates/bsnext_guards/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "bsnext_guards" +version = "0.1.0" +edition = "2021" + +[dependencies] +urlpattern = { version = "0.3.0" } +serde = { workspace = true } +tracing = { workspace = true } +axum = { workspace = true } +http = { workspace = true } \ No newline at end of file diff --git a/crates/bsnext_guards/src/lib.rs b/crates/bsnext_guards/src/lib.rs new file mode 100644 index 0000000..5e54faf --- /dev/null +++ b/crates/bsnext_guards/src/lib.rs @@ -0,0 +1,12 @@ +use crate::path_matcher::PathMatcher; + +pub mod path_matcher; +pub mod route_guard; + +#[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] +#[serde(untagged)] +pub enum MatcherList { + None, + Item(PathMatcher), + Items(Vec), +} diff --git a/crates/bsnext_resp/src/path_matcher.rs b/crates/bsnext_guards/src/path_matcher.rs similarity index 59% rename from crates/bsnext_resp/src/path_matcher.rs rename to crates/bsnext_guards/src/path_matcher.rs index e9e58ff..76c1063 100644 --- a/crates/bsnext_resp/src/path_matcher.rs +++ b/crates/bsnext_guards/src/path_matcher.rs @@ -67,62 +67,7 @@ impl PathMatcher { #[cfg(test)] mod test { - use super::*; - use crate::inject_addition::{AdditionPosition, InjectAddition}; - use crate::inject_opts::{InjectOpts, Injection, InjectionItem, MatcherList}; - - #[test] - fn test_path_matchers() { - #[derive(Debug, serde::Deserialize)] - struct A { - inject: InjectOpts, - } - let input = r#" -inject: - append: lol - only: - - /*.css - - pathname: /*.css -"#; - let expected = A { - inject: InjectOpts::Item(InjectionItem { - inner: Injection::Addition(InjectAddition { - addition_position: AdditionPosition::Append("lol".to_string()), - }), - only: Some(MatcherList::Items(vec![ - PathMatcher::Str("/*.css".to_string()), - PathMatcher::Def(PathMatcherDef { - pathname: Some("/*.css".to_string()), - }), - ])), - }), - }; - let actual: Result = serde_yaml::from_str(input); - assert_eq!(actual.unwrap().inject, expected.inject); - } - - #[test] - fn test_path_matcher_single() { - #[derive(Debug, serde::Deserialize)] - struct A { - inject: InjectOpts, - } - let input = r#" - inject: - append: lol - only: /*.css - "#; - let expected = A { - inject: InjectOpts::Item(InjectionItem { - inner: Injection::Addition(InjectAddition { - addition_position: AdditionPosition::Append("lol".to_string()), - }), - only: Some(MatcherList::Item(PathMatcher::Str("/*.css".to_string()))), - }), - }; - let actual: Result = serde_yaml::from_str(input); - assert_eq!(actual.unwrap().inject, expected.inject); - } + use crate::path_matcher::PathMatcher; #[test] fn test_url_pattern() { diff --git a/crates/bsnext_guards/src/route_guard.rs b/crates/bsnext_guards/src/route_guard.rs new file mode 100644 index 0000000..b5f10f9 --- /dev/null +++ b/crates/bsnext_guards/src/route_guard.rs @@ -0,0 +1,7 @@ +use axum::extract::Request; +use http::Response; + +pub trait RouteGuard { + fn accept_req(&self, req: &Request) -> bool; + fn accept_res(&self, res: &Response) -> bool; +} diff --git a/crates/bsnext_html/src/html_writer.rs b/crates/bsnext_html/src/html_writer.rs index 62623d6..95b4ae3 100644 --- a/crates/bsnext_html/src/html_writer.rs +++ b/crates/bsnext_html/src/html_writer.rs @@ -1,5 +1,3 @@ -use bsnext_input::playground::Playground; -use bsnext_input::server_config::ServerConfig; use bsnext_input::{Input, InputWriter}; pub struct HtmlWriter; @@ -36,6 +34,8 @@ impl InputWriter for HtmlWriter { #[test] fn test_html_writer_for_playground() { + use bsnext_input::playground::Playground; + use bsnext_input::server_config::ServerConfig; let css = r#"body { background: red; }"#; diff --git a/crates/bsnext_html/src/lib.rs b/crates/bsnext_html/src/lib.rs index 66708c6..d02f096 100644 --- a/crates/bsnext_html/src/lib.rs +++ b/crates/bsnext_html/src/lib.rs @@ -17,7 +17,7 @@ impl InputCreation for HtmlFs { } fn from_input_str>(content: P, ctx: &InputCtx) -> Result> { - let input = playground_html_str_to_input(&content.as_ref(), ctx) + let input = playground_html_str_to_input(content.as_ref(), ctx) .map_err(|e| Box::new(InputError::HtmlError(e.to_string())))?; Ok(input) } @@ -45,14 +45,14 @@ fn playground_html_str_to_input(html: &str, ctx: &InputCtx) -> Result anyhow::Result<()> { let Some(server) = as_input.servers.get(0) else { return Err(anyhow::anyhow!("no server")); }; - let routes = server.routes(); + let routes = server.combined_routes(); let html = routes.get(0).unwrap(); let js = routes.get(1).unwrap(); let css = routes.get(2).unwrap(); diff --git a/crates/bsnext_input/Cargo.toml b/crates/bsnext_input/Cargo.toml index 7a07ae0..c754ead 100644 --- a/crates/bsnext_input/Cargo.toml +++ b/crates/bsnext_input/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] bsnext_resp = { path = "../bsnext_resp" } bsnext_tracing = { path = "../bsnext_tracing" } +bsnext_guards = { path = "../bsnext_guards" } miette = { workspace = true } diff --git a/crates/bsnext_input/src/lib.rs b/crates/bsnext_input/src/lib.rs index ebc3efd..ac303cf 100644 --- a/crates/bsnext_input/src/lib.rs +++ b/crates/bsnext_input/src/lib.rs @@ -120,15 +120,15 @@ impl InputCtx { pub fn first_id_or_named(&self) -> ServerIdentity { self.prev_server_ids .as_ref() - .and_then(|x| x.get(0)) + .and_then(|x| x.first()) .map(ToOwned::to_owned) - .unwrap_or_else(|| ServerIdentity::named()) + .unwrap_or_else(ServerIdentity::named) } pub fn first_id(&self) -> Option { self.prev_server_ids .as_ref() - .and_then(|x| x.get(0)) + .and_then(|x| x.first()) .map(ToOwned::to_owned) } diff --git a/crates/bsnext_input/src/playground.rs b/crates/bsnext_input/src/playground.rs index 5258c16..8b64d71 100644 --- a/crates/bsnext_input/src/playground.rs +++ b/crates/bsnext_input/src/playground.rs @@ -1,9 +1,10 @@ use crate::path_def::PathDef; use crate::route::{FallbackRoute, Opts, Route, RouteKind}; +use bsnext_guards::path_matcher::PathMatcher; +use bsnext_guards::MatcherList; use bsnext_resp::builtin_strings::{BuiltinStringDef, BuiltinStrings}; use bsnext_resp::inject_addition::{AdditionPosition, InjectAddition}; -use bsnext_resp::inject_opts::{InjectOpts, Injection, InjectionItem, MatcherList}; -use bsnext_resp::path_matcher::PathMatcher; +use bsnext_resp::inject_opts::{InjectOpts, Injection, InjectionItem}; #[derive(Debug, PartialEq, Default, Hash, Clone, serde::Deserialize, serde::Serialize)] pub struct Playground { diff --git a/crates/bsnext_input/src/server_config.rs b/crates/bsnext_input/src/server_config.rs index e008a48..e562fd5 100644 --- a/crates/bsnext_input/src/server_config.rs +++ b/crates/bsnext_input/src/server_config.rs @@ -31,7 +31,7 @@ impl ServerConfig { } routes } - pub fn routes(&self) -> &[Route] { + pub fn raw_routes(&self) -> &[Route] { &self.routes } } diff --git a/crates/bsnext_md/src/md_writer.rs b/crates/bsnext_md/src/md_writer.rs index a911f3d..694564d 100644 --- a/crates/bsnext_md/src/md_writer.rs +++ b/crates/bsnext_md/src/md_writer.rs @@ -43,7 +43,7 @@ fn _input_to_str(input: &Input) -> String { } } - for route in server_config.routes() { + for route in server_config.raw_routes() { let path_only = json!({"path": route.path.as_str()}); let route_yaml = serde_yaml::to_string(&path_only).expect("never fail here on route?"); chunks.push(fenced_route(&route_yaml)); diff --git a/crates/bsnext_resp/Cargo.toml b/crates/bsnext_resp/Cargo.toml index 62377bc..c71693e 100644 --- a/crates/bsnext_resp/Cargo.toml +++ b/crates/bsnext_resp/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -urlpattern = { version = "0.3.0" } +bsnext_guards = { path = "../bsnext_guards" } tracing = { workspace = true } axum = { workspace = true } diff --git a/crates/bsnext_resp/src/builtin_strings.rs b/crates/bsnext_resp/src/builtin_strings.rs index 9efc9c9..1dc8e3f 100644 --- a/crates/bsnext_resp/src/builtin_strings.rs +++ b/crates/bsnext_resp/src/builtin_strings.rs @@ -1,7 +1,8 @@ use crate::connector::Connector; use crate::debug::Debug; -use crate::injector_guard::{ByteReplacer, InjectorGuard}; +use crate::injector_guard::ByteReplacer; use axum::extract::Request; +use bsnext_guards::route_guard::RouteGuard; use http::Response; #[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] @@ -16,7 +17,7 @@ pub enum BuiltinStrings { Debug, } -impl InjectorGuard for BuiltinStringDef { +impl RouteGuard for BuiltinStringDef { fn accept_req(&self, req: &Request) -> bool { match self.name { BuiltinStrings::Connector => Connector.accept_req(req), diff --git a/crates/bsnext_resp/src/connector.rs b/crates/bsnext_resp/src/connector.rs index eec4cbd..cdbc8a6 100644 --- a/crates/bsnext_resp/src/connector.rs +++ b/crates/bsnext_resp/src/connector.rs @@ -1,12 +1,13 @@ -use crate::injector_guard::{ByteReplacer, InjectorGuard}; +use crate::injector_guard::ByteReplacer; use crate::RespMod; use axum::extract::Request; +use bsnext_guards::route_guard::RouteGuard; use http::Response; #[derive(Debug, Default)] pub struct Connector; -impl InjectorGuard for Connector { +impl RouteGuard for Connector { fn accept_req(&self, req: &Request) -> bool { RespMod::accepts_html(req) } diff --git a/crates/bsnext_resp/src/debug.rs b/crates/bsnext_resp/src/debug.rs index 46aca4b..0bd3807 100644 --- a/crates/bsnext_resp/src/debug.rs +++ b/crates/bsnext_resp/src/debug.rs @@ -1,11 +1,12 @@ -use crate::injector_guard::{ByteReplacer, InjectorGuard}; +use crate::injector_guard::ByteReplacer; use axum::extract::Request; +use bsnext_guards::route_guard::RouteGuard; use http::Response; #[derive(Debug, Default)] pub struct Debug; -impl InjectorGuard for Debug { +impl RouteGuard for Debug { fn accept_req(&self, req: &Request) -> bool { req.uri().path().contains("core.css") } diff --git a/crates/bsnext_resp/src/inject_addition.rs b/crates/bsnext_resp/src/inject_addition.rs index d2958c8..a8e3250 100644 --- a/crates/bsnext_resp/src/inject_addition.rs +++ b/crates/bsnext_resp/src/inject_addition.rs @@ -1,5 +1,6 @@ -use crate::injector_guard::{ByteReplacer, InjectorGuard}; +use crate::injector_guard::ByteReplacer; use axum::extract::Request; +use bsnext_guards::route_guard::RouteGuard; use http::Response; #[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] @@ -15,7 +16,7 @@ pub enum AdditionPosition { Prepend(String), } -impl InjectorGuard for InjectAddition { +impl RouteGuard for InjectAddition { fn accept_req(&self, _req: &Request) -> bool { true } diff --git a/crates/bsnext_resp/src/inject_opt_test/mod.rs b/crates/bsnext_resp/src/inject_opt_test/mod.rs index 1674dd8..02b2bdd 100644 --- a/crates/bsnext_resp/src/inject_opt_test/mod.rs +++ b/crates/bsnext_resp/src/inject_opt_test/mod.rs @@ -3,6 +3,8 @@ use crate::builtin_strings::{BuiltinStringDef, BuiltinStrings}; use crate::inject_addition::{AdditionPosition, InjectAddition}; use crate::inject_opts::{InjectOpts, Injection, InjectionItem, UnknownStringDef}; use crate::inject_replacement::{InjectReplacement, Pos}; +use bsnext_guards::path_matcher::{PathMatcher, PathMatcherDef}; +use bsnext_guards::MatcherList; #[test] fn test_inject_opts_bool() { @@ -186,3 +188,56 @@ fn test_inject_append_prepend() { let actual: Result = serde_yaml::from_str(input); assert_eq!(actual.unwrap().inject, expected.inject); } + +#[test] +fn test_path_matchers() { + #[derive(Debug, serde::Deserialize)] + struct A { + inject: InjectOpts, + } + let input = r#" +inject: + append: lol + only: + - /*.css + - pathname: /*.css +"#; + let expected = A { + inject: InjectOpts::Item(InjectionItem { + inner: Injection::Addition(InjectAddition { + addition_position: AdditionPosition::Append("lol".to_string()), + }), + only: Some(MatcherList::Items(vec![ + PathMatcher::Str("/*.css".to_string()), + PathMatcher::Def(PathMatcherDef { + pathname: Some("/*.css".to_string()), + }), + ])), + }), + }; + let actual: Result = serde_yaml::from_str(input); + assert_eq!(actual.unwrap().inject, expected.inject); +} + +#[test] +fn test_path_matcher_single() { + #[derive(Debug, serde::Deserialize)] + struct A { + inject: InjectOpts, + } + let input = r#" + inject: + append: lol + only: /*.css + "#; + let expected = A { + inject: InjectOpts::Item(InjectionItem { + inner: Injection::Addition(InjectAddition { + addition_position: AdditionPosition::Append("lol".to_string()), + }), + only: Some(MatcherList::Item(PathMatcher::Str("/*.css".to_string()))), + }), + }; + let actual: Result = serde_yaml::from_str(input); + assert_eq!(actual.unwrap().inject, expected.inject); +} diff --git a/crates/bsnext_resp/src/inject_opts.rs b/crates/bsnext_resp/src/inject_opts.rs index ddb324e..6708637 100644 --- a/crates/bsnext_resp/src/inject_opts.rs +++ b/crates/bsnext_resp/src/inject_opts.rs @@ -1,9 +1,10 @@ use crate::builtin_strings::{BuiltinStringDef, BuiltinStrings}; use crate::inject_addition::InjectAddition; use crate::inject_replacement::InjectReplacement; -use crate::injector_guard::{ByteReplacer, InjectorGuard}; -use crate::path_matcher::PathMatcher; +use crate::injector_guard::ByteReplacer; use axum::extract::Request; +use bsnext_guards::route_guard::RouteGuard; +use bsnext_guards::MatcherList; use http::Response; #[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] @@ -55,14 +56,6 @@ pub struct InjectionItem { pub only: Option, } -#[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] -#[serde(untagged)] -pub enum MatcherList { - None, - Item(PathMatcher), - Items(Vec), -} - #[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] #[serde(untagged)] pub enum Injection { @@ -77,7 +70,7 @@ pub struct UnknownStringDef { pub name: String, } -impl InjectorGuard for InjectionItem { +impl RouteGuard for InjectionItem { fn accept_req(&self, req: &Request) -> bool { // right now, we only support matching on the pathname let path_and_query = req.uri().path_and_query().expect("?"); diff --git a/crates/bsnext_resp/src/inject_replacement.rs b/crates/bsnext_resp/src/inject_replacement.rs index fd1cfe6..e2db864 100644 --- a/crates/bsnext_resp/src/inject_replacement.rs +++ b/crates/bsnext_resp/src/inject_replacement.rs @@ -1,5 +1,6 @@ -use crate::injector_guard::{ByteReplacer, InjectorGuard}; +use crate::injector_guard::ByteReplacer; use axum::extract::Request; +use bsnext_guards::route_guard::RouteGuard; use http::Response; #[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] @@ -17,7 +18,7 @@ pub enum Pos { Replace(String), } -impl InjectorGuard for InjectReplacement { +impl RouteGuard for InjectReplacement { fn accept_req(&self, _req: &Request) -> bool { true } diff --git a/crates/bsnext_resp/src/injector_guard.rs b/crates/bsnext_resp/src/injector_guard.rs index fc3140d..45a0e03 100644 --- a/crates/bsnext_resp/src/injector_guard.rs +++ b/crates/bsnext_resp/src/injector_guard.rs @@ -1,13 +1,8 @@ -use axum::extract::Request; +use bsnext_guards::route_guard::RouteGuard; use bytes::Bytes; -use http::{HeaderMap, Response}; +use http::HeaderMap; -pub trait InjectorGuard { - fn accept_req(&self, req: &Request) -> bool; - fn accept_res(&self, res: &Response) -> bool; -} - -pub trait ByteReplacer: InjectorGuard { +pub trait ByteReplacer: RouteGuard { fn apply(&self, body: &'_ str) -> Option; fn replace_bytes( diff --git a/crates/bsnext_resp/src/lib.rs b/crates/bsnext_resp/src/lib.rs index 4d82a41..a0a05a6 100644 --- a/crates/bsnext_resp/src/lib.rs +++ b/crates/bsnext_resp/src/lib.rs @@ -6,17 +6,17 @@ pub mod inject_addition; pub mod inject_opts; pub mod inject_replacement; pub mod injector_guard; -pub mod path_matcher; use crate::inject_opts::InjectionItem; #[cfg(test)] pub mod inject_opt_test; -use crate::injector_guard::{ByteReplacer, InjectorGuard}; +use crate::injector_guard::ByteReplacer; use axum::body::Body; use axum::extract::Request; use axum::middleware::Next; use axum::response::IntoResponse; use axum::Extension; +use bsnext_guards::route_guard::RouteGuard; use http::header::{ACCEPT, CONTENT_LENGTH, CONTENT_TYPE}; use http::{Response, StatusCode}; use http_body_util::BodyExt; From 1163a75568b79fe541346383f763e5182cf845a7 Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Wed, 27 Nov 2024 23:43:03 +0000 Subject: [PATCH 2/5] improve the API --- crates/bsnext_guards/src/lib.rs | 14 +- crates/bsnext_guards/src/path_matcher.rs | 129 ++++++++++++++---- crates/bsnext_resp/src/inject_opt_test/mod.rs | 10 +- crates/bsnext_resp/src/inject_opts.rs | 13 +- crates/bsnext_resp/src/lib.rs | 1 + 5 files changed, 126 insertions(+), 41 deletions(-) diff --git a/crates/bsnext_guards/src/lib.rs b/crates/bsnext_guards/src/lib.rs index 5e54faf..c0ca76e 100644 --- a/crates/bsnext_guards/src/lib.rs +++ b/crates/bsnext_guards/src/lib.rs @@ -1,12 +1,24 @@ use crate::path_matcher::PathMatcher; +use http::Uri; pub mod path_matcher; pub mod route_guard; -#[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] +#[derive(Debug, Default, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] #[serde(untagged)] pub enum MatcherList { + #[default] None, Item(PathMatcher), Items(Vec), } + +impl MatcherList { + pub fn test_uri(&self, uri: &Uri) -> bool { + match self { + MatcherList::None => true, + MatcherList::Item(matcher) => matcher.test_uri(uri), + MatcherList::Items(matchers) => matchers.iter().any(|m| m.test_uri(uri)), + } + } +} diff --git a/crates/bsnext_guards/src/path_matcher.rs b/crates/bsnext_guards/src/path_matcher.rs index 76c1063..87a9a6a 100644 --- a/crates/bsnext_guards/src/path_matcher.rs +++ b/crates/bsnext_guards/src/path_matcher.rs @@ -1,3 +1,5 @@ +use http::Uri; +use std::str::FromStr; use urlpattern::UrlPatternInit; use urlpattern::UrlPatternMatchInput; use urlpattern::{UrlPattern, UrlPatternOptions}; @@ -10,44 +12,79 @@ pub enum PathMatcher { } impl PathMatcher { + pub fn str(str: impl Into) -> Self { + Self::Str(str.into()) + } pub fn pathname(str: impl Into) -> Self { Self::Def(PathMatcherDef { pathname: Some(str.into()), + search: None, + }) + } + pub fn query(str: impl Into) -> Self { + Self::Def(PathMatcherDef { + pathname: None, + search: Some(str.into()), }) } } #[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] pub struct PathMatcherDef { - pub pathname: Option, + pub(crate) pathname: Option, + pub(crate) search: Option, } impl PathMatcher { - pub fn test(&self, uri: &str) -> bool { + pub fn test_uri(&self, uri: &Uri) -> bool { + let Some(path_and_query) = uri.path_and_query() else { + tracing::error!("how is this possible?"); + return false; + }; + + let path = path_and_query.path(); + let seary = path_and_query.query(); + let incoming = UrlPatternInit { - pathname: Some(uri.to_owned()), + pathname: Some(path.into()), + search: seary.map(ToOwned::to_owned), ..Default::default() }; - let to_pathname = match self { - PathMatcher::Str(str) => str.as_str(), - PathMatcher::Def(PathMatcherDef { - pathname: Some(str), - }) => str.as_str(), - PathMatcher::Def(PathMatcherDef { pathname: None }) => { - unreachable!("how can this occur?") + // convert the config into UrlPatternInit + // example: /style.css + let matching_options: UrlPatternInit = match self { + PathMatcher::Str(str) => { + if let Ok(uri) = &Uri::from_str(str) { + if let Some(pq) = uri.path_and_query() { + let path = pq.path(); + let query = pq.query(); + UrlPatternInit { + pathname: Some(path.into()), + search: query.map(ToOwned::to_owned), + ..Default::default() + } + } else { + tracing::error!("could not parse the matching string you gave {}", str); + Default::default() + } + } else { + tracing::error!("could not parse the matching string you gave {}", str); + Default::default() + } } - }; - tracing::trace!(?to_pathname, ?uri, "PathMatcher::Str"); - let init = UrlPatternInit { - pathname: Some(to_pathname.to_owned()), - ..Default::default() + PathMatcher::Def(PathMatcherDef { pathname, search }) => UrlPatternInit { + pathname: pathname.to_owned(), + search: search.to_owned(), + ..Default::default() + }, }; let opts = UrlPatternOptions::default(); - let Ok(pattern) = ::parse(init, opts) else { - tracing::error!(?to_pathname, "could not parse the input"); + let Ok(pattern) = ::parse(matching_options, opts) else { + tracing::error!("could not parse the input"); return false; }; + // dbg!(&incoming); match pattern.test(UrlPatternMatchInput::Init(incoming)) { Ok(true) => { tracing::trace!("matched!"); @@ -68,19 +105,63 @@ impl PathMatcher { #[cfg(test)] mod test { use crate::path_matcher::PathMatcher; + use http::Uri; #[test] - fn test_url_pattern() { + fn test_url_pattern_pathname() { let pm = PathMatcher::pathname("/"); - assert_eq!(pm.test("/"), true); + assert_eq!(pm.test_uri(&Uri::from_static("/")), true); let pm = PathMatcher::pathname("/*.css"); - assert_eq!(pm.test("/style.css"), true); + assert_eq!(pm.test_uri(&Uri::from_static("/style.css")), true); let pm = PathMatcher::pathname("/here/*.css"); - assert_eq!(pm.test("/style.css"), false); + assert_eq!(pm.test_uri(&Uri::from_static("/style.css")), false); let pm = PathMatcher::pathname("/**/*.css"); - assert_eq!(pm.test("/style.css"), true); + assert_eq!(pm.test_uri(&Uri::from_static("/style.css")), true); let pm = PathMatcher::pathname("/**/*.css"); - assert_eq!(pm.test("/a/b/c/--oopasxstyle.css"), true); - assert_eq!(pm.test("/a/b/c/--oopasxstyle.html"), false); + assert_eq!( + pm.test_uri(&Uri::from_static("/a/b/c/--oopasxstyle.css")), + true + ); + assert_eq!( + pm.test_uri(&Uri::from_static("/a/b/c/--oopasxstyle.html")), + false + ); + } + #[test] + fn test_url_pattern_query() { + let pm = PathMatcher::str("/?abc=true"); + assert_eq!(pm.test_uri(&Uri::from_static("/")), false); + assert_eq!(pm.test_uri(&Uri::from_static("/?def=true")), false); + assert_eq!(pm.test_uri(&Uri::from_static("/?abc=true")), true); + assert_eq!(pm.test_uri(&Uri::from_static("/?abc=")), false); + + let pm2 = PathMatcher::str("/**/*?delayms"); + assert_eq!(pm2.test_uri(&Uri::from_static("/?delayms")), true); + + let pm2 = PathMatcher::query("?*a*b*c*foo=bar"); + assert_eq!( + pm2.test_uri(&Uri::from_static("/?delay.ms=2000&a-b-c-foo=bar")), + true + ); + } + #[test] + fn test_url_pattern_str() { + let pm = PathMatcher::str("/"); + assert_eq!(pm.test_uri(&Uri::from_static("/")), true); + let pm = PathMatcher::str("/*.css"); + assert_eq!(pm.test_uri(&Uri::from_static("/style.css")), true); + let pm = PathMatcher::str("/here/*.css"); + assert_eq!(pm.test_uri(&Uri::from_static("/style.css")), false); + let pm = PathMatcher::str("/**/*.css"); + assert_eq!(pm.test_uri(&Uri::from_static("/style.css")), true); + let pm = PathMatcher::str("/**/*.css"); + assert_eq!( + pm.test_uri(&Uri::from_static("/a/b/c/--oopasxstyle.css")), + true + ); + assert_eq!( + pm.test_uri(&Uri::from_static("/a/b/c/--oopasxstyle.html")), + false + ); } } diff --git a/crates/bsnext_resp/src/inject_opt_test/mod.rs b/crates/bsnext_resp/src/inject_opt_test/mod.rs index 02b2bdd..fcd1476 100644 --- a/crates/bsnext_resp/src/inject_opt_test/mod.rs +++ b/crates/bsnext_resp/src/inject_opt_test/mod.rs @@ -3,7 +3,7 @@ use crate::builtin_strings::{BuiltinStringDef, BuiltinStrings}; use crate::inject_addition::{AdditionPosition, InjectAddition}; use crate::inject_opts::{InjectOpts, Injection, InjectionItem, UnknownStringDef}; use crate::inject_replacement::{InjectReplacement, Pos}; -use bsnext_guards::path_matcher::{PathMatcher, PathMatcherDef}; +use bsnext_guards::path_matcher::PathMatcher; use bsnext_guards::MatcherList; #[test] @@ -208,10 +208,8 @@ inject: addition_position: AdditionPosition::Append("lol".to_string()), }), only: Some(MatcherList::Items(vec![ - PathMatcher::Str("/*.css".to_string()), - PathMatcher::Def(PathMatcherDef { - pathname: Some("/*.css".to_string()), - }), + PathMatcher::str("/*.css"), + PathMatcher::pathname("/*.css"), ])), }), }; @@ -235,7 +233,7 @@ fn test_path_matcher_single() { inner: Injection::Addition(InjectAddition { addition_position: AdditionPosition::Append("lol".to_string()), }), - only: Some(MatcherList::Item(PathMatcher::Str("/*.css".to_string()))), + only: Some(MatcherList::Item(PathMatcher::str("/*.css"))), }), }; let actual: Result = serde_yaml::from_str(input); diff --git a/crates/bsnext_resp/src/inject_opts.rs b/crates/bsnext_resp/src/inject_opts.rs index 6708637..64dbf58 100644 --- a/crates/bsnext_resp/src/inject_opts.rs +++ b/crates/bsnext_resp/src/inject_opts.rs @@ -69,20 +69,13 @@ pub enum Injection { pub struct UnknownStringDef { pub name: String, } - impl RouteGuard for InjectionItem { fn accept_req(&self, req: &Request) -> bool { - // right now, we only support matching on the pathname - let path_and_query = req.uri().path_and_query().expect("?"); - let path = path_and_query.path(); - - let path_is_allowed = match self.only.as_ref() { + let uri_is_allowed = match self.only.as_ref() { None => true, - Some(MatcherList::None) => true, - Some(MatcherList::Item(matcher)) => matcher.test(path), - Some(MatcherList::Items(matchers)) => matchers.iter().any(|m| m.test(path)), + Some(ml) => ml.test_uri(req.uri()), }; - if path_is_allowed { + if uri_is_allowed { match &self.inner { Injection::BsLive(built_ins) => built_ins.accept_req(req), Injection::UnknownNamed(_) => todo!("accept_req Injection::UnknownNamed"), diff --git a/crates/bsnext_resp/src/lib.rs b/crates/bsnext_resp/src/lib.rs index a0a05a6..48d086b 100644 --- a/crates/bsnext_resp/src/lib.rs +++ b/crates/bsnext_resp/src/lib.rs @@ -6,6 +6,7 @@ pub mod inject_addition; pub mod inject_opts; pub mod inject_replacement; pub mod injector_guard; + use crate::inject_opts::InjectionItem; #[cfg(test)] pub mod inject_opt_test; From a1cd9b0b77fc15d9b5fe385555b7c4f758f11383 Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Sat, 30 Nov 2024 19:47:10 +0000 Subject: [PATCH 3/5] support runtime context --- crates/bsnext_core/examples/abc.rs | 2 + crates/bsnext_core/src/handler_stack.rs | 143 ++++++++++++++---- crates/bsnext_core/src/lib.rs | 1 + crates/bsnext_core/src/proxy_loader.rs | 3 +- crates/bsnext_core/src/raw_loader.rs | 9 +- crates/bsnext_core/src/runtime_ctx.rs | 24 +++ crates/bsnext_core/src/serve_dir.rs | 5 +- .../bsnext_core/src/server/handler_listen.rs | 6 +- .../bsnext_core/src/server/handler_patch.rs | 7 +- .../bsnext_core/src/server/router/common.rs | 5 +- crates/bsnext_core/src/server/state.rs | 2 + .../src/servers_supervisor/actor.rs | 3 + ...handler_stack__test__handler_stack_01.snap | 36 ++--- ...handler_stack__test__handler_stack_03.snap | 40 ++--- crates/bsnext_core/tests/server.rs | 3 + crates/bsnext_html/src/lib.rs | 2 + examples/basic/handler_stack.yml | 6 + 17 files changed, 218 insertions(+), 79 deletions(-) create mode 100644 crates/bsnext_core/src/runtime_ctx.rs diff --git a/crates/bsnext_core/examples/abc.rs b/crates/bsnext_core/examples/abc.rs index 44051eb..2b21311 100644 --- a/crates/bsnext_core/examples/abc.rs +++ b/crates/bsnext_core/examples/abc.rs @@ -1,4 +1,5 @@ use actix::Actor; +use bsnext_core::runtime_ctx::RuntimeCtx; use bsnext_core::server::actor::ServerActor; use bsnext_core::server::handler_listen::Listen; use bsnext_core::servers_supervisor::get_servers_handler::{GetServersMessage, IncomingEvents}; @@ -28,6 +29,7 @@ async fn main() { let a = s .send(Listen { + runtime_ctx: RuntimeCtx::default(), parent: parent.clone().recipient(), evt_receiver: parent.recipient(), }) diff --git a/crates/bsnext_core/src/handler_stack.rs b/crates/bsnext_core/src/handler_stack.rs index e40191f..e8acfd1 100644 --- a/crates/bsnext_core/src/handler_stack.rs +++ b/crates/bsnext_core/src/handler_stack.rs @@ -2,6 +2,7 @@ use crate::handlers::proxy::{proxy_handler, ProxyConfig}; use crate::not_found::not_found_service::not_found_loader; use crate::optional_layers::optional_layers; use crate::raw_loader::serve_raw_one; +use crate::runtime_ctx::RuntimeCtx; use crate::serve_dir::try_many_services_dir; use axum::handler::Handler; use axum::middleware::{from_fn, from_fn_with_state}; @@ -9,15 +10,17 @@ use axum::routing::{any, any_service, get_service, MethodRouter}; use axum::{Extension, Router}; use bsnext_input::route::{DirRoute, FallbackRoute, Opts, ProxyRoute, RawRoute, Route, RouteKind}; use std::collections::HashMap; +use std::path::{Path, PathBuf}; use tower_http::services::{ServeDir, ServeFile}; #[derive(Debug, PartialEq)] pub enum HandlerStack { None, // todo: make this a separate thing - Raw { - raw: RawRoute, - opts: Opts, + Raw(RawRouteOpts), + RawAndDirs { + raw: RawRouteOpts, + dirs: Vec, }, Dirs(Vec), Proxy { @@ -38,8 +41,14 @@ pub struct DirRouteOpts { fallback_route: Option, } +#[derive(Debug, PartialEq)] +pub struct RawRouteOpts { + raw_route: RawRoute, + opts: Opts, +} + impl DirRouteOpts { - pub fn as_serve_dir(&self) -> ServeDir { + pub fn as_serve_dir(&self, cwd: &Path) -> ServeDir { match &self.dir_route.base { Some(base_dir) => { tracing::trace!( @@ -50,8 +59,19 @@ impl DirRouteOpts { ServeDir::new(base_dir.join(&self.dir_route.dir)) } None => { - tracing::trace!("no root given, using `{}` directly", self.dir_route.dir); - ServeDir::new(&self.dir_route.dir) + let pb = PathBuf::from(&self.dir_route.dir); + if pb.is_absolute() { + tracing::trace!("no root given, using `{}` directly", self.dir_route.dir); + ServeDir::new(&self.dir_route.dir) + } else { + let joined = cwd.join(pb); + tracing::trace!( + "prepending the current directory to relative path {} {}", + cwd.display(), + joined.display() + ); + ServeDir::new(joined) + } } } .append_index_html_on_directories(true) @@ -102,7 +122,7 @@ impl RouteMap { } } - pub fn into_router(self) -> Router { + pub fn into_router(self, ctx: &RuntimeCtx) -> Router { let mut router = Router::new(); tracing::trace!("processing `{}` different routes", self.mapping.len()); @@ -114,8 +134,8 @@ impl RouteMap { route_list.len() ); - let stack = routes_to_stack(&route_list); - let path_router = stack_to_router(&path, stack); + let stack = routes_to_stack(route_list); + let path_router = stack_to_router(&path, stack, ctx); tracing::trace!("will merge router at path: `{path}`"); router = router.merge(path_router); @@ -128,10 +148,10 @@ impl RouteMap { pub fn append_stack(state: HandlerStack, route: Route) -> HandlerStack { match state { HandlerStack::None => match route.kind { - RouteKind::Raw(raw_route) => HandlerStack::Raw { - raw: raw_route, + RouteKind::Raw(raw_route) => HandlerStack::Raw(RawRouteOpts { + raw_route, opts: route.opts, - }, + }), RouteKind::Proxy(new_proxy_route) => HandlerStack::Proxy { proxy: new_proxy_route, opts: route.opts, @@ -140,15 +160,22 @@ pub fn append_stack(state: HandlerStack, route: Route) -> HandlerStack { HandlerStack::Dirs(vec![DirRouteOpts::new(dir, route.opts, route.fallback)]) } }, - HandlerStack::Raw { raw, opts } => match route.kind { + HandlerStack::Raw(RawRouteOpts { raw_route, opts }) => match route.kind { // if a second 'raw' is seen, just use it, discarding the previous - RouteKind::Raw(raw_route) => HandlerStack::Raw { - raw: raw_route, + RouteKind::Raw(raw_route) => HandlerStack::Raw(RawRouteOpts { + raw_route, opts: route.opts, + }), + RouteKind::Dir(dir) => HandlerStack::RawAndDirs { + dirs: vec![DirRouteOpts::new(dir, route.opts, None)], + raw: RawRouteOpts { raw_route, opts }, }, // 'raw' handlers never get updated - _ => HandlerStack::Raw { raw, opts }, + _ => HandlerStack::Raw(RawRouteOpts { raw_route, opts }), }, + HandlerStack::RawAndDirs { .. } => { + todo!("support RawAndDirs") + } HandlerStack::Dirs(mut dirs) => match route.kind { RouteKind::Dir(next_dir) => { dirs.push(DirRouteOpts::new(next_dir, route.opts, route.fallback)); @@ -204,22 +231,31 @@ pub fn fallback_to_layered_method_router(route: FallbackRoute) -> MethodRouter { } } -pub fn routes_to_stack(routes: &[Route]) -> HandlerStack { - routes.iter().fold(HandlerStack::None, |s, route| { - append_stack(s, route.clone()) - }) +pub fn routes_to_stack(routes: Vec) -> HandlerStack { + routes + .into_iter() + .fold(HandlerStack::None, |s, route| append_stack(s, route)) } -pub fn stack_to_router(path: &str, stack: HandlerStack) -> Router { +pub fn stack_to_router(path: &str, stack: HandlerStack, ctx: &RuntimeCtx) -> Router { match stack { HandlerStack::None => unreachable!(), - HandlerStack::Raw { raw, opts } => { - let svc = any_service(serve_raw_one.with_state(raw)); + HandlerStack::Raw(RawRouteOpts { raw_route, opts }) => { + let svc = any_service(serve_raw_one.with_state(raw_route)); let out = optional_layers(svc, &opts); Router::new().route_service(path, out) } + HandlerStack::RawAndDirs { + dirs, + raw: RawRouteOpts { raw_route, opts }, + } => { + let svc = any_service(serve_raw_one.with_state(raw_route)); + let raw_out = optional_layers(svc, &opts); + let service = serve_dir_layer(&dirs, Router::new(), ctx); + Router::new().route(path, raw_out).fallback_service(service) + } HandlerStack::Dirs(dirs) => { - let service = serve_dir_layer(&dirs, Router::new()); + let service = serve_dir_layer(&dirs, Router::new(), ctx); Router::new() .nest_service(path, service) .layer(from_fn(not_found_loader)) @@ -236,26 +272,30 @@ pub fn stack_to_router(path: &str, stack: HandlerStack) -> Router { Router::new().nest_service(path, optional_layers(as_service, &opts)) } HandlerStack::DirsProxy { dirs, proxy, opts } => { - let proxy_router = stack_to_router(path, HandlerStack::Proxy { proxy, opts }); - let r1 = serve_dir_layer(&dirs, Router::new().fallback_service(proxy_router)); + let proxy_router = stack_to_router(path, HandlerStack::Proxy { proxy, opts }, ctx); + let r1 = serve_dir_layer(&dirs, Router::new().fallback_service(proxy_router), ctx); Router::new().nest_service(path, r1) } } } -fn serve_dir_layer(dir_list_with_opts: &[DirRouteOpts], initial: Router) -> Router { +fn serve_dir_layer( + dir_list_with_opts: &[DirRouteOpts], + initial: Router, + ctx: &RuntimeCtx, +) -> Router { let serve_dir_items = dir_list_with_opts .iter() .map(|dir_route| match &dir_route.fallback_route { None => { - let serve_dir_service = dir_route.as_serve_dir(); + let serve_dir_service = dir_route.as_serve_dir(ctx.cwd()); let service = get_service(serve_dir_service); optional_layers(service, &dir_route.opts) } Some(fallback) => { let stack = fallback_to_layered_method_router(fallback.clone()); let serve_dir_service = dir_route - .as_serve_dir() + .as_serve_dir(ctx.cwd()) .fallback(stack) .call_fallback_on_method_not_allowed(true); let service = any_service(serve_dir_service); @@ -272,6 +312,7 @@ mod test { use super::*; use crate::server::router::common::to_resp_parts_and_body; use axum::body::Body; + use std::env::current_dir; use bsnext_input::Input; use http::Request; @@ -287,9 +328,10 @@ mod test { .servers .iter() .find(|x| x.identity.is_named("raw")) + .map(ToOwned::to_owned) .unwrap(); - let actual = routes_to_stack(&first.routes); + let actual = routes_to_stack(first.routes); assert_debug_snapshot!(actual); Ok(()) } @@ -302,9 +344,10 @@ mod test { .servers .iter() .find(|x| x.identity.is_named("2dirs+proxy")) + .map(ToOwned::to_owned) .unwrap(); - let actual = routes_to_stack(&first.routes); + let actual = routes_to_stack(first.routes); assert_debug_snapshot!(actual); Ok(()) @@ -317,9 +360,10 @@ mod test { .servers .iter() .find(|s| s.identity.is_named("raw+opts")) + .map(ToOwned::to_owned) .unwrap(); - let actual = routes_to_stack(&first.routes); + let actual = routes_to_stack(first.routes); assert_debug_snapshot!(actual); Ok(()) @@ -338,7 +382,7 @@ mod test { .unwrap(); let route_map = RouteMap::new_from_routes(&first.routes); - let router = route_map.into_router(); + let router = route_map.into_router(&RuntimeCtx::default()); let request = Request::get("/styles.css").body(Body::empty())?; // Define the request @@ -349,6 +393,39 @@ mod test { assert_eq!(body, "body { background: red }"); } + Ok(()) + } + #[tokio::test] + async fn test_raw_with_dir_fallback() -> anyhow::Result<()> { + let yaml = include_str!("../../../examples/basic/handler_stack.yml"); + let input = serde_yaml::from_str::(&yaml)?; + + { + let first = input + .servers + .iter() + .find(|x| x.identity.is_named("raw+dir")) + .unwrap(); + + let route_map = RouteMap::new_from_routes(&first.routes); + let router = route_map.into_router(&RuntimeCtx::default()); + let raw_request = Request::get("/").body(Body::empty())?; + let response = router.oneshot(raw_request).await?; + let (parts, body) = to_resp_parts_and_body(response).await; + assert_eq!(body, "hello world!"); + + let cwd = current_dir().unwrap(); + let cwd = cwd.ancestors().nth(2).unwrap(); + let ctx = RuntimeCtx::new(cwd); + let route_map = RouteMap::new_from_routes(&first.routes); + let router = route_map.into_router(&ctx); + let dir_request = Request::get("/script.js").body(Body::empty())?; + let response = router.oneshot(dir_request).await?; + let (_parts, body) = to_resp_parts_and_body(response).await; + let expected = include_str!("../../../examples/basic/public/script.js"); + assert_eq!(body, expected); + } + Ok(()) } } diff --git a/crates/bsnext_core/src/lib.rs b/crates/bsnext_core/src/lib.rs index e842677..751d4de 100644 --- a/crates/bsnext_core/src/lib.rs +++ b/crates/bsnext_core/src/lib.rs @@ -10,5 +10,6 @@ pub mod optional_layers; pub mod panic_handler; pub mod proxy_loader; pub mod raw_loader; +pub mod runtime_ctx; pub mod serve_dir; pub mod ws; diff --git a/crates/bsnext_core/src/proxy_loader.rs b/crates/bsnext_core/src/proxy_loader.rs index 55420dc..ab5eb82 100644 --- a/crates/bsnext_core/src/proxy_loader.rs +++ b/crates/bsnext_core/src/proxy_loader.rs @@ -2,6 +2,7 @@ mod test { use crate::handler_stack::RouteMap; + use crate::runtime_ctx::RuntimeCtx; use crate::server::router::common::{test_proxy, to_resp_parts_and_body}; use axum::body::Body; use axum::extract::Request; @@ -37,7 +38,7 @@ mod test { { let routes = serde_yaml::from_str::>(&routes_input)?; let router = RouteMap::new_from_routes(&routes) - .into_router() + .into_router(&RuntimeCtx::default()) .layer(Extension(client)); let expected_body = "target!"; diff --git a/crates/bsnext_core/src/raw_loader.rs b/crates/bsnext_core/src/raw_loader.rs index 8ba0be0..d81aa45 100644 --- a/crates/bsnext_core/src/raw_loader.rs +++ b/crates/bsnext_core/src/raw_loader.rs @@ -52,6 +52,7 @@ async fn raw_resp_for(uri: Uri, route: &RawRoute) -> impl IntoResponse { mod raw_test { use super::*; use crate::handler_stack::RouteMap; + use crate::runtime_ctx::RuntimeCtx; use crate::server::router::common::to_resp_parts_and_body; use bsnext_input::route::Route; use tower::ServiceExt; @@ -75,7 +76,7 @@ mod raw_test { { let routes: Vec = serde_yaml::from_str(routes_input)?; - let router = RouteMap::new_from_routes(&routes).into_router(); + let router = RouteMap::new_from_routes(&routes).into_router(&RuntimeCtx::default()); // Define the request let request = Request::get("/route1").body(Body::empty())?; // Make a one-shot request on the router @@ -86,7 +87,7 @@ mod raw_test { { let routes: Vec = serde_yaml::from_str(routes_input)?; - let router = RouteMap::new_from_routes(&routes).into_router(); + let router = RouteMap::new_from_routes(&routes).into_router(&RuntimeCtx::default()); // Define the request let request = Request::get("/raw1").body(Body::empty())?; // Make a one-shot request on the router @@ -97,7 +98,7 @@ mod raw_test { { let routes: Vec = serde_yaml::from_str(routes_input)?; - let router = RouteMap::new_from_routes(&routes).into_router(); + let router = RouteMap::new_from_routes(&routes).into_router(&RuntimeCtx::default()); // Define the request let request = Request::get("/json").body(Body::empty())?; // Make a one-shot request on the router @@ -108,7 +109,7 @@ mod raw_test { { let routes: Vec = serde_yaml::from_str(routes_input)?; - let router = RouteMap::new_from_routes(&routes).into_router(); + let router = RouteMap::new_from_routes(&routes).into_router(&RuntimeCtx::default()); // Define the request let request = Request::get("/sse").body(Body::empty())?; // Make a one-shot request on the router diff --git a/crates/bsnext_core/src/runtime_ctx.rs b/crates/bsnext_core/src/runtime_ctx.rs new file mode 100644 index 0000000..7aa6a14 --- /dev/null +++ b/crates/bsnext_core/src/runtime_ctx.rs @@ -0,0 +1,24 @@ +use std::env::current_dir; +use std::path::PathBuf; + +#[derive(Debug, Clone)] +pub struct RuntimeCtx { + cwd: PathBuf, +} + +impl Default for RuntimeCtx { + fn default() -> Self { + Self { + cwd: current_dir().expect("failed to get current directory"), + } + } +} + +impl RuntimeCtx { + pub fn new>(path: P) -> Self { + Self { cwd: path.into() } + } + pub fn cwd(&self) -> &PathBuf { + &self.cwd + } +} diff --git a/crates/bsnext_core/src/serve_dir.rs b/crates/bsnext_core/src/serve_dir.rs index 6f7f4a4..492885a 100644 --- a/crates/bsnext_core/src/serve_dir.rs +++ b/crates/bsnext_core/src/serve_dir.rs @@ -61,6 +61,7 @@ mod test { use crate::handler_stack::RouteMap; use crate::server::router::common::to_resp_parts_and_body; + use crate::runtime_ctx::RuntimeCtx; use bsnext_input::route::Route; use std::env::current_dir; @@ -84,7 +85,7 @@ mod test { let routes = serde_yaml::from_str::>(&routes_input)?; { - let router = RouteMap::new_from_routes(&routes).into_router(); + let router = RouteMap::new_from_routes(&routes).into_router(&RuntimeCtx::default()); let expected_body = include_str!("../../../examples/basic/public/index.html"); // Define the request @@ -96,7 +97,7 @@ mod test { } { - let router = RouteMap::new_from_routes(&routes).into_router(); + let router = RouteMap::new_from_routes(&routes).into_router(&RuntimeCtx::default()); let expected_body = include_str!("../../../examples/kitchen-sink/input.html"); // Define the request diff --git a/crates/bsnext_core/src/server/handler_listen.rs b/crates/bsnext_core/src/server/handler_listen.rs index bdf2487..dc6d2b7 100644 --- a/crates/bsnext_core/src/server/handler_listen.rs +++ b/crates/bsnext_core/src/server/handler_listen.rs @@ -1,4 +1,5 @@ use crate::handler_stack::RouteMap; +use crate::runtime_ctx::RuntimeCtx; use crate::server::actor::ServerActor; use crate::server::router::make_router; use crate::server::state::ServerState; @@ -17,6 +18,7 @@ use tokio::sync::{oneshot, RwLock}; pub struct Listen { pub parent: Recipient, pub evt_receiver: Recipient, + pub runtime_ctx: RuntimeCtx, } impl actix::Handler for ServerActor { @@ -30,12 +32,14 @@ impl actix::Handler for ServerActor { let h1 = handle.clone(); let h2 = handle.clone(); - let router = RouteMap::new_from_routes(&self.config.combined_routes()).into_router(); + let router = + RouteMap::new_from_routes(&self.config.combined_routes()).into_router(&msg.runtime_ctx); let app_state = Arc::new(ServerState { // parent: , routes: Arc::new(RwLock::new(self.config.combined_routes())), raw_router: Arc::new(RwLock::new(router)), + runtime_ctx: msg.runtime_ctx, client_config: Arc::new(RwLock::new(self.config.clients.clone())), id: self.config.identity.as_id(), parent: Some(msg.parent.clone()), diff --git a/crates/bsnext_core/src/server/handler_patch.rs b/crates/bsnext_core/src/server/handler_patch.rs index fd823f7..6f1b80f 100644 --- a/crates/bsnext_core/src/server/handler_patch.rs +++ b/crates/bsnext_core/src/server/handler_patch.rs @@ -41,12 +41,17 @@ impl actix::Handler for ServerActor { .clients .changeset_for(&msg.server_config.clients); + let Some(app_state) = &self.app_state else { + unreachable!("app_state should be set"); + }; + // todo(alpha): use the actix dedicated methods for async state mutation? Box::pin({ let c = span.clone(); + let ctx = app_state.runtime_ctx.clone(); async move { - let router = RouteMap::new_from_routes(&routes).into_router(); + let router = RouteMap::new_from_routes(&routes).into_router(&ctx); let mut mut_raw_router = app_state_clone.raw_router.write().await; *mut_raw_router = router; drop(mut_raw_router); diff --git a/crates/bsnext_core/src/server/router/common.rs b/crates/bsnext_core/src/server/router/common.rs index c2d0639..71dc803 100644 --- a/crates/bsnext_core/src/server/router/common.rs +++ b/crates/bsnext_core/src/server/router/common.rs @@ -3,6 +3,7 @@ use crate::server::state::ServerState; use std::net::SocketAddr; use crate::handler_stack::RouteMap; +use crate::runtime_ctx::RuntimeCtx; use axum::body::Body; use axum::extract::Request; use axum::response::Response; @@ -22,8 +23,10 @@ use tower::ServiceExt; pub fn into_state(val: ServerConfig) -> ServerState { let (sender, _) = tokio::sync::broadcast::channel::(10); - let router = RouteMap::new_from_routes(&val.combined_routes()).into_router(); + let runtime_ctx = RuntimeCtx::default(); + let router = RouteMap::new_from_routes(&val.combined_routes()).into_router(&runtime_ctx); ServerState { + runtime_ctx: RuntimeCtx::default(), routes: Arc::new(RwLock::new(val.combined_routes())), raw_router: Arc::new(RwLock::new(router)), client_config: Arc::new(RwLock::new(val.clients.clone())), diff --git a/crates/bsnext_core/src/server/state.rs b/crates/bsnext_core/src/server/state.rs index e75b858..dc64f30 100644 --- a/crates/bsnext_core/src/server/state.rs +++ b/crates/bsnext_core/src/server/state.rs @@ -1,3 +1,4 @@ +use crate::runtime_ctx::RuntimeCtx; use crate::servers_supervisor::get_servers_handler::{GetServersMessage, IncomingEvents}; use actix::Recipient; use axum::Router; @@ -11,6 +12,7 @@ use tokio::sync::{broadcast, RwLock}; #[derive(Clone)] pub struct ServerState { pub routes: Arc>>, + pub runtime_ctx: RuntimeCtx, pub raw_router: Arc>, pub client_config: Arc>, pub id: u64, diff --git a/crates/bsnext_core/src/servers_supervisor/actor.rs b/crates/bsnext_core/src/servers_supervisor/actor.rs index f724121..44c5a54 100644 --- a/crates/bsnext_core/src/servers_supervisor/actor.rs +++ b/crates/bsnext_core/src/servers_supervisor/actor.rs @@ -8,6 +8,7 @@ use bsnext_input::Input; use std::collections::HashSet; use std::net::SocketAddr; +use crate::runtime_ctx::RuntimeCtx; use crate::server::handler_listen::Listen; use crate::server::handler_patch::Patch; use bsnext_dto::internal::{ @@ -117,6 +118,8 @@ impl ServersSupervisor { let c = server_config.clone(); actor_addr .send(Listen { + // todo: tie this to the input somehow? + runtime_ctx: RuntimeCtx::default(), parent: self_addr.clone().recipient(), evt_receiver: self_addr.clone().recipient(), }) diff --git a/crates/bsnext_core/src/snapshots/bsnext_core__handler_stack__test__handler_stack_01.snap b/crates/bsnext_core/src/snapshots/bsnext_core__handler_stack__test__handler_stack_01.snap index 69f7894..de7684c 100644 --- a/crates/bsnext_core/src/snapshots/bsnext_core__handler_stack__test__handler_stack_01.snap +++ b/crates/bsnext_core/src/snapshots/bsnext_core__handler_stack__test__handler_stack_01.snap @@ -2,21 +2,23 @@ source: crates/bsnext_core/src/handler_stack.rs expression: actual --- -Raw { - raw: Raw { - raw: "body { background: red }", +Raw( + RawRouteOpts { + raw_route: Raw { + raw: "body { background: red }", + }, + opts: Opts { + cors: None, + delay: None, + watch: Bool( + true, + ), + inject: Bool( + true, + ), + headers: None, + cache: Prevent, + compression: None, + }, }, - opts: Opts { - cors: None, - delay: None, - watch: Bool( - true, - ), - inject: Bool( - true, - ), - headers: None, - cache: Prevent, - compression: None, - }, -} +) diff --git a/crates/bsnext_core/src/snapshots/bsnext_core__handler_stack__test__handler_stack_03.snap b/crates/bsnext_core/src/snapshots/bsnext_core__handler_stack__test__handler_stack_03.snap index 53109cd..c359413 100644 --- a/crates/bsnext_core/src/snapshots/bsnext_core__handler_stack__test__handler_stack_03.snap +++ b/crates/bsnext_core/src/snapshots/bsnext_core__handler_stack__test__handler_stack_03.snap @@ -2,25 +2,27 @@ source: crates/bsnext_core/src/handler_stack.rs expression: actual --- -Raw { - raw: Raw { - raw: "console.log(\"hello world!\")", - }, - opts: Opts { - cors: Some( - Cors( +Raw( + RawRouteOpts { + raw_route: Raw { + raw: "console.log(\"hello world!\")", + }, + opts: Opts { + cors: Some( + Cors( + true, + ), + ), + delay: None, + watch: Bool( + true, + ), + inject: Bool( true, ), - ), - delay: None, - watch: Bool( - true, - ), - inject: Bool( - true, - ), - headers: None, - cache: Prevent, - compression: None, + headers: None, + cache: Prevent, + compression: None, + }, }, -} +) diff --git a/crates/bsnext_core/tests/server.rs b/crates/bsnext_core/tests/server.rs index 56ca312..832cd72 100644 --- a/crates/bsnext_core/tests/server.rs +++ b/crates/bsnext_core/tests/server.rs @@ -1,4 +1,5 @@ use actix::Actor; +use bsnext_core::runtime_ctx::RuntimeCtx; use bsnext_core::server::actor::ServerActor; use bsnext_core::server::handler_listen::Listen; use bsnext_core::servers_supervisor::file_changed_handler::FilesChanged; @@ -32,6 +33,7 @@ async fn system_test_01() { let listen_result = s .send(Listen { + runtime_ctx: RuntimeCtx::default(), parent: parent.clone().recipient(), evt_receiver: parent.recipient(), }) @@ -70,6 +72,7 @@ async fn system_test_02() { let list_result = server_actor .send(Listen { + runtime_ctx: RuntimeCtx::default(), parent: parent.clone().recipient(), evt_receiver: parent.clone().recipient(), }) diff --git a/crates/bsnext_html/src/lib.rs b/crates/bsnext_html/src/lib.rs index d02f096..ecb4e96 100644 --- a/crates/bsnext_html/src/lib.rs +++ b/crates/bsnext_html/src/lib.rs @@ -31,9 +31,11 @@ fn playground_html_str_to_input(html: &str, ctx: &InputCtx) -> Result Date: Mon, 2 Dec 2024 21:32:18 +0000 Subject: [PATCH 4/5] support serve-dir + playground on same route --- crates/bsnext_input/src/server_config.rs | 12 ++++++++---- examples/basic/playground.yml | 4 +++- examples/markdown/playground.md | 3 +++ package-lock.json | 18 ++++++++++++------ package.json | 2 +- tests/playground.spec.ts | 17 ++++++++++++++++- ...d-markdown-playground-1-chromium-darwin.txt | 16 ++++++++++++++++ 7 files changed, 59 insertions(+), 13 deletions(-) create mode 100644 tests/playground.spec.ts-snapshots/examples-markdown-playground-md-markdown-playground-1-chromium-darwin.txt diff --git a/crates/bsnext_input/src/server_config.rs b/crates/bsnext_input/src/server_config.rs index e562fd5..313ec3a 100644 --- a/crates/bsnext_input/src/server_config.rs +++ b/crates/bsnext_input/src/server_config.rs @@ -25,11 +25,15 @@ impl ServerConfig { /// All regular routes, plus dynamically added ones (for example, through a playground) /// pub fn combined_routes(&self) -> Vec { - let mut routes = self.routes.clone(); - if let Some(playground) = &self.playground { - routes.extend(playground.as_routes()) + let routes = self.routes.clone(); + match &self.playground { + None => self.routes.clone(), + Some(playground) => { + let mut pg_routes = playground.as_routes(); + pg_routes.extend(routes); + pg_routes + } } - routes } pub fn raw_routes(&self) -> &[Route] { &self.routes diff --git a/examples/basic/playground.yml b/examples/basic/playground.yml index 8673ac2..f1797d6 100644 --- a/examples/basic/playground.yml +++ b/examples/basic/playground.yml @@ -1,5 +1,8 @@ servers: - name: playground + routes: + - path: / + dir: examples/basic/public playground: html: |
@@ -7,7 +10,6 @@ servers:
css: | @import url("/reset.css"); - :root { border: 50px solid pink; height: 100vh; diff --git a/examples/markdown/playground.md b/examples/markdown/playground.md index 9242404..3756f04 100644 --- a/examples/markdown/playground.md +++ b/examples/markdown/playground.md @@ -1,6 +1,9 @@ --- servers: - name: playground + routes: + - path: / + dir: examples/basic/public --- ```html playground diff --git a/package-lock.json b/package-lock.json index 5d266cd..91ed30f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ }, "devDependencies": { "@napi-rs/cli": "^2.18.3", - "@playwright/test": "^1.46.1", + "@playwright/test": "^1.49.0", "@types/node": "20.17.6", "ava": "^6.0.1", "esbuild": "^0.24.0", @@ -1246,11 +1246,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.46.1", + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0.tgz", + "integrity": "sha512-DMulbwQURa8rNIQrf94+jPJQ4FmOVdpE5ZppRNvWVjvhC+6sOeo28r8MgIpQRYouXRtt/FCCXU7zn20jnHR4Qw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.46.1" + "playwright": "1.49.0" }, "bin": { "playwright": "cli.js" @@ -4396,11 +4398,13 @@ } }, "node_modules/playwright": { - "version": "1.46.1", + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0.tgz", + "integrity": "sha512-eKpmys0UFDnfNb3vfsf8Vx2LEOtflgRebl0Im2eQQnYMA4Aqd+Zw8bEOB+7ZKvN76901mRnqdsiOGKxzVTbi7A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.46.1" + "playwright-core": "1.49.0" }, "bin": { "playwright": "cli.js" @@ -4413,7 +4417,9 @@ } }, "node_modules/playwright-core": { - "version": "1.46.1", + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0.tgz", + "integrity": "sha512-R+3KKTQF3npy5GTiKH/T+kdhoJfJojjHESR1YEWhYuEKRVfVaxH3+4+GvXE5xyCngCxhxnykk0Vlah9v8fs3jA==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index 19085a0..54589a5 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ ], "devDependencies": { "@napi-rs/cli": "^2.18.3", - "@playwright/test": "^1.46.1", + "@playwright/test": "^1.49.0", "ava": "^6.0.1", "typescript": "^5.6.3", "zod": "^3.23.8", diff --git a/tests/playground.spec.ts b/tests/playground.spec.ts index 8e1ca73..6b921ba 100644 --- a/tests/playground.spec.ts +++ b/tests/playground.spec.ts @@ -15,8 +15,23 @@ test.describe( test("markdown playground", async ({ page, bs }) => { const text: string[] = []; page.on("console", (msg) => text.push(msg.text())); + + /** + * This first request should be picked up by the playground + */ await page.goto(bs.path("/"), { waitUntil: "networkidle" }); - expect(text).toContain("Hello from playground.md"); + const html = await page.locator(":root").innerHTML(); + expect(html).toMatchSnapshot(); + + await test.step("serving dir alongside playground", async () => { + /** + * This index.html request should bypass the playground and serve from disk + */ + await page.goto(bs.path("/index.html"), { + waitUntil: "networkidle", + }); + await page.getByText("Edit me! - a full HTML").waitFor(); + }); }); }, ); diff --git a/tests/playground.spec.ts-snapshots/examples-markdown-playground-md-markdown-playground-1-chromium-darwin.txt b/tests/playground.spec.ts-snapshots/examples-markdown-playground-md-markdown-playground-1-chromium-darwin.txt new file mode 100644 index 0000000..b1b5e38 --- /dev/null +++ b/tests/playground.spec.ts-snapshots/examples-markdown-playground-md-markdown-playground-1-chromium-darwin.txt @@ -0,0 +1,16 @@ + + + + Browsersync Live - Playground + + + + +
+ Hello world! +
+ + + + + \ No newline at end of file From ba4b56271e6a93ea7f2835f8555b5d4965f05c52 Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Wed, 4 Dec 2024 22:13:21 +0000 Subject: [PATCH 5/5] support serve dir in HTML playground --- Cargo.lock | 7 ++ crates/bsnext_core/src/handler_stack.rs | 6 +- crates/bsnext_html/src/lib.rs | 20 ++++++ crates/bsnext_html/tests/html_playground.rs | 28 ++++++++ ...ayground__html_playground_with_meta-2.snap | 10 +++ ...playground__html_playground_with_meta.snap | 7 ++ crates/bsnext_input/Cargo.toml | 1 + crates/bsnext_input/src/lib.rs | 2 + crates/bsnext_input/src/route.rs | 5 ++ crates/bsnext_input/src/route_cli.rs | 63 ++++++++++++++++++ ...ext_input__route_cli__test__serve_dir.snap | 10 +++ crates/bsnext_system/src/args.rs | 2 + examples/basic/public/bg-01.jpg | Bin 0 -> 86080 bytes examples/html/playground.html | 4 ++ tests/playground.spec.ts | 6 ++ 15 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 crates/bsnext_html/tests/snapshots/html_playground__html_playground_with_meta-2.snap create mode 100644 crates/bsnext_html/tests/snapshots/html_playground__html_playground_with_meta.snap create mode 100644 crates/bsnext_input/src/route_cli.rs create mode 100644 crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir.snap create mode 100644 examples/basic/public/bg-01.jpg diff --git a/Cargo.lock b/Cargo.lock index db76d94..fbbb963 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -590,6 +590,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "shell-words", "thiserror", "toml", ] @@ -2689,6 +2690,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "signal-hook" version = "0.3.17" diff --git a/crates/bsnext_core/src/handler_stack.rs b/crates/bsnext_core/src/handler_stack.rs index e8acfd1..50dd2c7 100644 --- a/crates/bsnext_core/src/handler_stack.rs +++ b/crates/bsnext_core/src/handler_stack.rs @@ -232,9 +232,7 @@ pub fn fallback_to_layered_method_router(route: FallbackRoute) -> MethodRouter { } pub fn routes_to_stack(routes: Vec) -> HandlerStack { - routes - .into_iter() - .fold(HandlerStack::None, |s, route| append_stack(s, route)) + routes.into_iter().fold(HandlerStack::None, append_stack) } pub fn stack_to_router(path: &str, stack: HandlerStack, ctx: &RuntimeCtx) -> Router { @@ -411,7 +409,7 @@ mod test { let router = route_map.into_router(&RuntimeCtx::default()); let raw_request = Request::get("/").body(Body::empty())?; let response = router.oneshot(raw_request).await?; - let (parts, body) = to_resp_parts_and_body(response).await; + let (_parts, body) = to_resp_parts_and_body(response).await; assert_eq!(body, "hello world!"); let cwd = current_dir().unwrap(); diff --git a/crates/bsnext_html/src/lib.rs b/crates/bsnext_html/src/lib.rs index ecb4e96..387f669 100644 --- a/crates/bsnext_html/src/lib.rs +++ b/crates/bsnext_html/src/lib.rs @@ -1,4 +1,5 @@ use bsnext_input::playground::Playground; +use bsnext_input::route::Route; use bsnext_input::server_config::{ServerConfig, ServerIdentity}; use bsnext_input::{Input, InputCreation, InputCtx, InputError}; use std::fs::read_to_string; @@ -45,6 +46,8 @@ fn playground_html_str_to_input(html: &str, ctx: &InputCtx) -> Result Result { + let joined = format!("{} {}", name, content); + let r = Route::from_cli_str(joined); + if let Ok(route) = r { + routes.push(route); + node_ids_to_remove.push(x.id()); + } + } + _ => todo!("not supported!"), + } + } + for node_id in node_ids_to_remove { document.tree.get_mut(node_id).unwrap().detach(); } @@ -88,6 +107,7 @@ fn playground_html_str_to_input(html: &str, ctx: &InputCtx) -> Result anyhow::Result<()> { assert_eq!(first.identity, ident); Ok(()) } + +const INPUT_WITH_META: &str = r#" + +
+

Test!

+ +
"#; + +#[test] +fn test_html_playground_with_meta() -> anyhow::Result<()> { + let ident = ServerIdentity::Address { + bind_address: String::from("0.0.0.0:8080"), + }; + let input_args = InputArgs { port: Some(8080) }; + let ctx = InputCtx::new(&[], Some(input_args)); + let as_input = HtmlFs::from_input_str(INPUT_WITH_META, &ctx)?; + let first = as_input.servers.get(0).unwrap(); + assert_eq!(first.identity, ident); + let routes = first.combined_routes(); + let found = routes + .iter() + .find(|x| matches!(x.kind, RouteKind::Dir(..))) + .expect("must find dir"); + assert_debug_snapshot!(found.path); + assert_debug_snapshot!(found.kind); + Ok(()) +} diff --git a/crates/bsnext_html/tests/snapshots/html_playground__html_playground_with_meta-2.snap b/crates/bsnext_html/tests/snapshots/html_playground__html_playground_with_meta-2.snap new file mode 100644 index 0000000..ba24976 --- /dev/null +++ b/crates/bsnext_html/tests/snapshots/html_playground__html_playground_with_meta-2.snap @@ -0,0 +1,10 @@ +--- +source: crates/bsnext_html/tests/html_playground.rs +expression: found.kind +--- +Dir( + DirRoute { + dir: "examples/basic/public", + base: None, + }, +) diff --git a/crates/bsnext_html/tests/snapshots/html_playground__html_playground_with_meta.snap b/crates/bsnext_html/tests/snapshots/html_playground__html_playground_with_meta.snap new file mode 100644 index 0000000..c6f107d --- /dev/null +++ b/crates/bsnext_html/tests/snapshots/html_playground__html_playground_with_meta.snap @@ -0,0 +1,7 @@ +--- +source: crates/bsnext_html/tests/html_playground.rs +expression: found.path +--- +PathDef { + inner: "/", +} diff --git a/crates/bsnext_input/Cargo.toml b/crates/bsnext_input/Cargo.toml index c754ead..24d0be4 100644 --- a/crates/bsnext_input/Cargo.toml +++ b/crates/bsnext_input/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" bsnext_resp = { path = "../bsnext_resp" } bsnext_tracing = { path = "../bsnext_tracing" } bsnext_guards = { path = "../bsnext_guards" } +shell-words = { version = "1.1.0" } miette = { workspace = true } diff --git a/crates/bsnext_input/src/lib.rs b/crates/bsnext_input/src/lib.rs index ac303cf..49e0cc0 100644 --- a/crates/bsnext_input/src/lib.rs +++ b/crates/bsnext_input/src/lib.rs @@ -12,6 +12,8 @@ pub mod input_test; pub mod path_def; pub mod playground; pub mod route; + +pub mod route_cli; pub mod route_manifest; pub mod server_config; pub mod startup; diff --git a/crates/bsnext_input/src/route.rs b/crates/bsnext_input/src/route.rs index d3d7108..d9bffa9 100644 --- a/crates/bsnext_input/src/route.rs +++ b/crates/bsnext_input/src/route.rs @@ -1,4 +1,5 @@ use crate::path_def::PathDef; +use crate::route_cli::RouteCli; use crate::watch_opts::WatchOpts; use bsnext_resp::cache_opts::CacheOpts; use bsnext_resp::inject_opts::InjectOpts; @@ -78,6 +79,10 @@ impl Route { pub fn path(&self) -> &str { self.path.as_str() } + pub fn from_cli_str>(a: A) -> Result { + let cli = RouteCli::try_from_cli_str(a)?; + cli.try_into() + } } #[derive(Debug, Hash, PartialEq, Clone, serde::Deserialize, serde::Serialize)] diff --git a/crates/bsnext_input/src/route_cli.rs b/crates/bsnext_input/src/route_cli.rs new file mode 100644 index 0000000..8931ae0 --- /dev/null +++ b/crates/bsnext_input/src/route_cli.rs @@ -0,0 +1,63 @@ +use crate::path_def::PathDef; +use crate::route::{DirRoute, Route, RouteKind}; +use clap::Parser; +use shell_words::split; + +#[derive(clap::Parser, Debug)] +#[command(version)] +pub struct RouteCli { + #[command(subcommand)] + command: SubCommands, +} + +impl RouteCli { + pub fn try_from_cli_str>(a: A) -> Result { + let as_args = split(a.as_ref())?; + RouteCli::try_parse_from(as_args).map_err(|e| anyhow::anyhow!(e)) + } +} + +impl TryInto for RouteCli { + type Error = anyhow::Error; + + fn try_into(self) -> Result { + Ok(match self.command { + SubCommands::ServeDir { dir, path } => { + let mut route = Route::default(); + route.path = PathDef::try_new(path)?; + route.kind = RouteKind::Dir(DirRoute { dir, base: None }); + route + } + }) + } +} + +#[derive(Debug, clap::Subcommand)] +pub enum SubCommands { + /// does testing things + ServeDir { + /// lists test values + #[arg(short, long)] + path: String, + #[arg(short, long)] + dir: String, + }, +} + +#[cfg(test)] +mod test { + use super::*; + use clap::Parser; + + use shell_words::split; + #[test] + fn test_serve_dir() -> anyhow::Result<()> { + let input = "bslive serve-dir --path=/ --dir=examples/basic/public"; + let as_args = split(input)?; + let parsed = RouteCli::try_parse_from(as_args)?; + let as_route: Result = parsed.try_into(); + dbg!(&as_route); + // assert_debug_snapshot!(parsed); + Ok(()) + } +} diff --git a/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir.snap b/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir.snap new file mode 100644 index 0000000..6e262f6 --- /dev/null +++ b/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir.snap @@ -0,0 +1,10 @@ +--- +source: crates/bsnext_input/src/route_cli.rs +expression: parsed +--- +RouteCli { + command: ServeDir { + path: "/", + dir: "examples/basic/public", + }, +} diff --git a/crates/bsnext_system/src/args.rs b/crates/bsnext_system/src/args.rs index 8baf3bd..07ce89e 100644 --- a/crates/bsnext_system/src/args.rs +++ b/crates/bsnext_system/src/args.rs @@ -2,6 +2,8 @@ use crate::Example; use bsnext_input::target::TargetKind; use bsnext_tracing::{LogLevel, OutputFormat}; +// bslive route --path=/ --dir= + #[derive(clap::Parser, Debug)] #[command(version, name = "Browsersync Live")] pub struct Args { diff --git a/examples/basic/public/bg-01.jpg b/examples/basic/public/bg-01.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a4af5d0fb0a2feb257d74fdf2ff9496f5791b968 GIT binary patch literal 86080 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

k}(W>4=Z7&|ZfOmqAy4|jYWm#6;H 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

a*g(T?_@KWUMdzn(=O2=FXH*D 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 literal 0 HcmV?d00001 diff --git a/examples/html/playground.html b/examples/html/playground.html index a234b1f..22b3b93 100644 --- a/examples/html/playground.html +++ b/examples/html/playground.html @@ -1,3 +1,5 @@ + +