diff --git a/CHANGELOG.md b/CHANGELOG.md index 96980f6ac448c5f85608076df4dcdda6dad91e2d..44e9edb0c03179f7b740df112e10f3f34591d8e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added +- [DEV] Initial test scaffolding + ## [0.2.0] ### Added diff --git a/Cargo.lock b/Cargo.lock index 1ed2edd6b716fcad485fe45ec5979810872714df..b084fa9e4b12cfd4b2fc6d40cf3350029df31ddd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -235,6 +235,11 @@ dependencies = [ "unicode-normalization 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "interpolate_idents" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "isatty" version = "0.1.7" @@ -639,6 +644,7 @@ dependencies = [ "docopt 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)", "formdata 0.12.2 (registry+https://github.com/rust-lang/crates.io-index)", "hyper 0.10.13 (registry+https://github.com/rust-lang/crates.io-index)", + "interpolate_idents 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", "rhai 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "rocket 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", @@ -858,6 +864,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum httparse 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "c2f407128745b78abc95c0ffbe4e5d37427fdc0d45470710cfef8c44522a2e37" "checksum hyper 0.10.13 (registry+https://github.com/rust-lang/crates.io-index)" = "368cb56b2740ebf4230520e2b90ebb0461e69034d85d1945febd9b3971426db2" "checksum idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "014b298351066f1512874135335d62a789ffe78a9974f94b43ed5621951eaf7d" +"checksum interpolate_idents 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c038526b1556151b78f71b3e4cb107cf58c4dfc426a64a398c61f76a42a4e08" "checksum isatty 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "a118a53ba42790ef25c82bb481ecf36e2da892646cccd361e69a6bb881e19398" "checksum itoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c069bbec61e1ca5a596166e55dfe4773ff745c3d16b700013bcaff9a6df2c682" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" diff --git a/Cargo.toml b/Cargo.toml index f181189886d045f97ae2617cb6e62dd371954fc7..f92735d5f80859c70b91584235cf589435caeec0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,3 +37,6 @@ hyper = "0.10" rand = "0.3" rhai = "0.7" serde_yaml = "0.7.3" + +[dev-dependencies] +interpolate_idents = "0.2.4" diff --git a/example/files/adorable-puppy.jpg b/example/files/adorable-puppy.jpg new file mode 100644 index 0000000000000000000000000000000000000000..caa08afbae4082a0efccfa24060f06485d8009e9 Binary files /dev/null and b/example/files/adorable-puppy.jpg differ diff --git a/example/files/attribution.csv b/example/files/attribution.csv new file mode 100644 index 0000000000000000000000000000000000000000..66bd3e19ebd286eef77994042c0b72185eaf4cfa --- /dev/null +++ b/example/files/attribution.csv @@ -0,0 +1,3 @@ +file,author/attribution, link +adorable-puppy.jpg,Photo by mentatdgt from Pexels,https://www.pexels.com/photo/white-brown-and-black-shih-tzu-puppy-936317/ +math.aswm,Github: Hanks10100,https://github.com/Hanks10100/wasm-examples diff --git a/example/files/math.wasm b/example/files/math.wasm new file mode 100644 index 0000000000000000000000000000000000000000..4140e01798748e33698b7f0eb0b0ef8c0bcc52df Binary files /dev/null and b/example/files/math.wasm differ diff --git a/example/index.html b/example/index.html index 24ac4ba12acf44383c15860708aaaecbe5a2d922..080c0fc49cfb6a8e4ab7847645034944714928e4 100644 --- a/example/index.html +++ b/example/index.html @@ -3,10 +3,11 @@ <head> <meta charset="UTF-8"> <title>Swerve Example</title> - <link rel="stylesheet" href="/css/styles.css"> + <link rel="stylesheet" href="css/styles.css"> </head> <body> <h1>It's Swervin' Time</h1> <p>This page is part of the swerve example, and includes a stylesheet and stuff.</p> + <script async src="js/say_hello.js"></script> </body> </html> \ No newline at end of file diff --git a/example/js/say_hello.js b/example/js/say_hello.js new file mode 100644 index 0000000000000000000000000000000000000000..d0e38b2bfa1dd85d51824d730d50606e193c6c01 --- /dev/null +++ b/example/js/say_hello.js @@ -0,0 +1,3 @@ +document.addEventListener('DOMContentLoaded', function() { + console.log("The dom has loaded!") +}) \ No newline at end of file diff --git a/src/cli/cli.rs b/src/cli/cli.rs index 578dc8c79d2e00028b89073ce3cd6c361c8ca533..f2272d2e0012db5ced535db6f556b78ff3f9079a 100644 --- a/src/cli/cli.rs +++ b/src/cli/cli.rs @@ -1,5 +1,6 @@ pub use std::path::PathBuf; pub use std::env::current_dir; +pub use std::default::Default; pub const USAGE: &'static str = " Static file swerver and api mocker for local development. @@ -21,7 +22,7 @@ Web Server Options: -c=<path>, --config=<path> Path to the .swerve config file -t=<num>, --threads=<num> Number of worker threads to use for serving files; defaults to 32 -Data Handling Options +Data Handling Options: -u, --upload Support file uploads to '/upload' -U=<path>, --upload-path=<path> Set the url path that will accept file uploads. Implies 'upload' flag if not present @@ -54,4 +55,22 @@ impl Args { ).to_string_lossy().into_owned()) ) } +} + +impl Default for Args { + fn default() -> Self { + Args { + flag_dir: Some(String::from("")), + flag_port: Some(8000), + flag_config: None, + flag_threads: Some(32), + flag_address: Some(String::from("localhost")), + flag_help: false, + flag_quiet: false, + flag_no_index: false, + flag_upload: false, + flag_upload_path: None, + flag_license: false, + } + } } \ No newline at end of file diff --git a/src/cli/config_file.rs b/src/cli/config_file.rs index 09e088fa23f7569ed865d94b76c33758726a02f5..7723ae5209ec491e3939a6f9b0c46116881003be 100644 --- a/src/cli/config_file.rs +++ b/src/cli/config_file.rs @@ -3,9 +3,8 @@ use std::convert::AsRef; use std::io::prelude::*; use std::io; use std::fs::File; -use std::io::BufReader; use std::default::Default; -use serde::{Deserialize, Deserializer, de::{self, Error}}; +use serde::{Deserialize, Deserializer, de}; use std::fmt; use serde_yaml as yaml; diff --git a/src/lib.rs b/src/lib.rs index 26cb6505f0715e0b53800fd011be5f3a08d27f17..d384507b3f4e2159bf5c86a41948d4061d515628 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,4 +13,5 @@ extern crate rand; pub mod cli; pub mod routing; -pub mod scripting; \ No newline at end of file +pub mod scripting; +pub mod server; diff --git a/src/main.rs b/src/main.rs index 13e981d28cee3881a790339766f6c4c399fcbeba..832dc7725849cf292ae07853e1bc77283a2d9158 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,98 +8,10 @@ extern crate docopt; extern crate swerve; extern crate rhai; -use rhai::Engine; - -use std::{path, process, io}; -use std::fs::{self, File}; +use std::process; use docopt::Docopt; -use rocket::response::NamedFile; -use rocket::http::ContentType; -use rocket::{Response, Request}; use swerve::cli; -use swerve::routing; -use std::io::BufReader; -use std::path::{Path, PathBuf}; -use rocket::response::Responder; - -struct TypedFile { - file: File, - content_type: Option<ContentType>, - path: PathBuf, -} - -impl TypedFile { - pub fn open<P: AsRef<Path>>(path: P, content_type: Option<rocket::http::ContentType>) -> TypedFile { - let file = File::open(path.as_ref()).unwrap(); - TypedFile { file, content_type, path: (*path.as_ref()).to_path_buf() } - } -} - -impl rocket::response::Responder<'static> for TypedFile { - fn respond_to(self, _: &Request) -> Result<Response<'static>, rocket::http::Status> { - let mut response = Response::new(); - if let Some(content_type) = self.content_type { - response.set_header(content_type); - } else { - response.set_header(ContentType::from_extension(&self.path.extension().unwrap().to_string_lossy()).unwrap()); - } - response.set_streamed_body(BufReader::new(self.file)); - Ok(response) - } -} - -#[get("/")] -fn serve_root(args: rocket::State<cli::Args>) -> Option<TypedFile> { - serve_files(None, args) -} - -#[get("/<file..>")] -fn serve_files(file: Option<path::PathBuf>, args: rocket::State<cli::Args>) -> Option<TypedFile> { - let stub = match file { - Some(path) => path, - None => path::PathBuf::from(""), - }; - - let path = args.get_dir().join(stub); - - let meta = match fs::metadata(&path) { - Ok(metadata) => metadata, - _ => return None, - }; - - if meta.is_dir() && !args.flag_no_index { - Some(TypedFile::open(path.join("index.html"), None)) - } else { - if &path.extension().unwrap().to_string_lossy() == "wasm" { - Some( TypedFile::open(path, Some(ContentType::new("application", "wasm")))) - } else { - Some(TypedFile::open(path, None)) - } - } -} - -fn config_from_args(args: cli::Args, config: cli::SwerveConfig) -> rocket::Config { - let mut builder = rocket::Config::build(rocket::config::Environment::Development); - if let Some(threads) = args.flag_threads { - builder = builder.workers(threads); - } else { - builder = builder.workers(config.server.threads); - } - - if let Some(port) = args.flag_port { - builder = builder.port(port); - } else { - builder = builder.port(config.server.port); - } - - if let Some(address) = args.flag_address { - builder = builder.address(address); - } else { - builder = builder.address(config.server.address); - } - - builder.finalize().unwrap() -} +use swerve::server; fn main() { let args: cli::Args = Docopt::new(cli::USAGE) @@ -107,18 +19,8 @@ fn main() { .unwrap_or_else(|e| e.exit()); let is_quiet = args.flag_quiet; - macro_rules! printq { - ($( $x:expr ),+) => { - { - if !is_quiet { - println!($($x),*); - } - } - } - } - if args.flag_help { - printq!("{}", cli::USAGE); + if !is_quiet { println!("{}", cli::USAGE); } process::exit(0); } @@ -133,38 +35,6 @@ fn main() { std::process::exit(2); }); - let server_config = config_from_args(args.clone(), swerve_config.clone()); - // printq!("{:?}", swerve_config); - printq!(""); - - let mut server = rocket::custom(server_config, false) - .manage(args.clone()) - .manage(swerve_config); - - if let Some(ref upload_path) = args.flag_upload_path { - printq!("[SETUP] Accepting uploads at {}", upload_path); - server = server.mount(upload_path, routes![swerve::routing::mock_upload::to_file]); - } else if args.flag_upload { - printq!("[SETUP] Accepting uploads at /upload"); - server = server.mount("/upload", routes![swerve::routing::mock_upload::to_file]); - } - server = server.mount("/", routes![ - serve_root, - serve_files, - routing::scripting::route_script - ]); - - if !args.flag_quiet { - server = server.attach(rocket::fairing::AdHoc::on_launch(move |rckt| { - let config = rckt.config(); - println!("[SETUP] Swerve is configured with {} worker threads", config.workers); - println!("[SETUP] Swerving files from http://{}:{}\n", config.address, config.port); - })) - .attach(rocket::fairing::AdHoc::on_response(|req, _res| { - println!("[REQUEST] {} {}", req.method(), req.uri()); - })); - } - { - server.launch(); - } + let server = server::create_server(args.clone(), swerve_config.clone()); + server.launch(); } \ No newline at end of file diff --git a/src/routing/core.rs b/src/routing/core.rs new file mode 100644 index 0000000000000000000000000000000000000000..a847d1dae81df0f8bd6342ebe0c12ec47bf94fea --- /dev/null +++ b/src/routing/core.rs @@ -0,0 +1,35 @@ +use cli; +use std::fs; +use std::path; +use rocket::{self, http::ContentType}; +use routing::request::TypedFile; + +#[get("/")] +fn serve_root(args: rocket::State<cli::Args>) -> Option<TypedFile> { + serve_files(None, args) +} + +#[get("/<file..>")] +fn serve_files(file: Option<path::PathBuf>, args: rocket::State<cli::Args>) -> Option<TypedFile> { + let stub = match file { + Some(path) => path, + None => path::PathBuf::from(""), + }; + + let path = args.get_dir().join(stub); + + let meta = match fs::metadata(&path) { + Ok(metadata) => metadata, + _ => return None, + }; + + if meta.is_dir() && !args.flag_no_index { + Some(TypedFile::open(path.join("index.html"), None)) + } else { + if &path.extension().unwrap().to_string_lossy() == "wasm" { + Some( TypedFile::open(path, Some(ContentType::new("application", "wasm")))) + } else { + Some(TypedFile::open(path, None)) + } + } +} diff --git a/src/routing/mod.rs b/src/routing/mod.rs index b7054b18f5f05411ac7ed75c68e51f918937b6ec..d3fa15ff5030dccc4162756be7c3e0f264472775 100644 --- a/src/routing/mod.rs +++ b/src/routing/mod.rs @@ -1,3 +1,4 @@ pub mod mock_upload; pub mod request; -pub mod scripting; \ No newline at end of file +pub mod scripting; +pub mod core; diff --git a/src/routing/request.rs b/src/routing/request.rs index 0ebd52b826307a61b380ed6d630983a8a6d084a4..643abe5ddd56afc24fc01c5970e9bc72b5ea768c 100644 --- a/src/routing/request.rs +++ b/src/routing/request.rs @@ -1,6 +1,10 @@ +use rocket::{self, Outcome, http, Response}; use rocket::request::{FromRequest, Request}; -use rocket::{Outcome, http}; +use rocket::http::ContentType; use hyper::header::Headers; +use std::path::{Path, PathBuf}; +use std::io::BufReader; +use std::fs::File; #[derive(Debug)] pub struct ConvertedHeaders { @@ -26,4 +30,30 @@ impl <'a, 'req>FromRequest<'a, 'req> for ConvertedHeaders { inner: hyper_headers }) } -} \ No newline at end of file +} + +pub struct TypedFile { + file: File, + content_type: Option<ContentType>, + path: PathBuf, +} + +impl TypedFile { + pub fn open<P: AsRef<Path>>(path: P, content_type: Option<rocket::http::ContentType>) -> TypedFile { + let file = File::open(path.as_ref()).unwrap(); + TypedFile { file, content_type, path: (*path.as_ref()).to_path_buf() } + } +} + +impl rocket::response::Responder<'static> for TypedFile { + fn respond_to(self, _: &Request) -> Result<Response<'static>, rocket::http::Status> { + let mut response = Response::new(); + if let Some(content_type) = self.content_type { + response.set_header(content_type); + } else { + response.set_header(ContentType::from_extension(&self.path.extension().unwrap().to_string_lossy()).unwrap()); + } + response.set_streamed_body(BufReader::new(self.file)); + Ok(response) + } +} diff --git a/src/server/mod.rs b/src/server/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..1604eface9ba9609f408325378e3fa55eac7927d --- /dev/null +++ b/src/server/mod.rs @@ -0,0 +1,3 @@ +mod server; + +pub use self::server::create_server; \ No newline at end of file diff --git a/src/server/server.rs b/src/server/server.rs new file mode 100644 index 0000000000000000000000000000000000000000..37a1b9453d9ddb28ee397b4b369509eb4c239f63 --- /dev/null +++ b/src/server/server.rs @@ -0,0 +1,63 @@ +use rocket::{self, Rocket, Config}; +use cli::{Args, SwerveConfig}; +use routing; + +pub fn create_server(args: Args, config: SwerveConfig) -> Rocket { + let server_config = server_config_from_input(args.clone(), config.clone()); + let mut server = Rocket::custom(server_config, false) + .manage(args.clone()) + .manage(config.clone()); + + let quiet = args.flag_quiet; + + if let Some(ref upload_path) = args.flag_upload_path { + if !quiet { println!("[SETUP] Accepting uploads at {}", upload_path) } + server = server.mount(upload_path, routes![routing::mock_upload::to_file]); + } else if args.flag_upload { + if !quiet { println!("[SETUP] Accepting uploads at /upload") } + server = server.mount("/upload", routes![routing::mock_upload::to_file]); + } + + server = server.mount("/", routes![ + routing::core::serve_root, + routing::core::serve_files, + routing::scripting::route_script + ]); + + + if !quiet { + server = server.attach(rocket::fairing::AdHoc::on_launch(move |rckt| { + let conf = rckt.config(); + println!("[SETUP] Swerve is configured with {} worker threads", conf.workers); + println!("[SETUP] Swerving files from http://{}:{}\n", conf.address, conf.port); + })) + .attach(rocket::fairing::AdHoc::on_response(|req, _res| { + println!("[REQUEST] {} {}", req.method(), req.uri()); + })); + } + + server +} + +fn server_config_from_input(args: Args, config: SwerveConfig) -> Config { + let mut builder = Config::build(rocket::config::Environment::Development); + if let Some(threads) = args.flag_threads { + builder = builder.workers(threads); + } else { + builder = builder.workers(config.server.threads); + } + + if let Some(port) = args.flag_port { + builder = builder.port(port); + } else { + builder = builder.port(config.server.port); + } + + if let Some(address) = args.flag_address { + builder = builder.address(address); + } else { + builder = builder.address(config.server.address); + } + + builder.finalize().unwrap() +} \ No newline at end of file diff --git a/tests/basic_operations.rs b/tests/basic_operations.rs new file mode 100644 index 0000000000000000000000000000000000000000..b98a39a44ccf09c06ac705802eea02fa8affaf9e --- /dev/null +++ b/tests/basic_operations.rs @@ -0,0 +1,28 @@ + +extern crate rocket; +extern crate swerve; + +use swerve::cli::{Args, SwerveConfig}; +use swerve::server::create_server; + +use rocket::local::Client; +use rocket::http::Status; + +const INDEX_PAGE: &'static str = include_str!("../example/index.html"); + +#[test] +fn test_serves_index() { + let args = Args { + flag_dir: Some(String::from("example")), + ..Args::default() + }; + let config = SwerveConfig::default(); + + let server = create_server(args, config); + + let client = Client::new(server).expect("valid server instance"); + let mut response = client.get("/").dispatch(); + + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.body_string(), Some(INDEX_PAGE.into())); +} \ No newline at end of file diff --git a/tests/content_types.rs b/tests/content_types.rs new file mode 100644 index 0000000000000000000000000000000000000000..c87a97191b0009fb507996fb9904a8717f126815 --- /dev/null +++ b/tests/content_types.rs @@ -0,0 +1,56 @@ +#![feature(plugin)] +#![plugin(interpolate_idents)] + +extern crate rocket; +extern crate swerve; + +use swerve::cli::{Args, SwerveConfig}; +use swerve::server::create_server; + +use rocket::local::Client; +use rocket::http::ContentType; + +macro_rules! test_type { + ($name:ident, $path:expr, $content_path:expr) => (interpolate_idents! { + #[test] + fn [returns_some_type_for_ $name]() { + let args = Args { + flag_dir: Some(String::from("example")), + flag_quiet: true, + ..Args::default() + }; + let config = SwerveConfig::default(); + + let server = create_server(args, config); + let client = Client::new(server).expect("valid server instance"); + let response = client.get($path).dispatch(); + + assert_eq!(response.content_type(), Some($content_path)); + } + }); + + ($name:ident, $path:expr) => (interpolate_idents! { + #[test] + fn [returns_no_type_for_ $name]() { + let args = Args { + flag_dir: Some(String::from("example")), + flag_quiet: true, + ..Args::default() + }; + let config = SwerveConfig::default(); + + let server = create_server(args, config); + let client = Client::new(server).expect("valid server instance"); + let response = client.get($path).dispatch(); + + assert_eq!(response.content_type(), None); + } + }); +} + +test_type!(html, "/index.html", ContentType::HTML); +test_type!(css, "/css/styles.css", ContentType::CSS); +test_type!(javascript, "/js/say_hello.js", ContentType::JavaScript); +test_type!(csv, "/files/attribution.csv", ContentType::CSV); +test_type!(image_jpeg, "/files/adorable-puppy.jpg", ContentType::JPEG); +test_type!(web_assembly, "/files/math.wasm", ContentType::new("application", "wasm"));