Skip to content
Snippets Groups Projects
Unverified Commit cbee4a2f authored by Louis's avatar Louis :fire: Committed by GitHub
Browse files

Merge pull request #2 from Commander-lol/Scripting

### Added

- Lua scripting support as a route handler in the `config.yml` file
parents aab29e80 6d35988a
No related branches found
No related tags found
No related merge requests found
Showing
with 609 additions and 269 deletions
...@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ...@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
### Added ### Added
- [DEV] Initial test scaffolding - [DEV] Initial test scaffolding
- Lua scripting support as a route handler in the `config.yml` file
## [0.2.0] ## [0.2.0]
...@@ -34,4 +35,4 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ...@@ -34,4 +35,4 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Serve files from the target directory - Serve files from the target directory
- Serve index files from directories - Serve index files from directories
- Configure number of worker threads - Configure number of worker threads
- Bind to custom address and port - Bind to custom address and port
\ No newline at end of file
This diff is collapsed.
...@@ -35,8 +35,8 @@ docopt = "0.8" ...@@ -35,8 +35,8 @@ docopt = "0.8"
formdata = "0.12.2" formdata = "0.12.2"
hyper = "0.10" hyper = "0.10"
rand = "0.3" rand = "0.3"
rhai = "0.7"
serde_yaml = "0.7.3" serde_yaml = "0.7.3"
rlua = "0.13.0"
[dev-dependencies] [dev-dependencies]
interpolate_idents = "0.2.4" interpolate_idents = "0.2.4"
local accountDB = {}
accountDB['fancy-account'] = {
name = "Fancy Account",
widgets = { barfoo = "The best widget in town", bipbop = "A really good widget"}
}
accountDB['less-fancy-account'] = {
name = "Not Quite As Fancy Account",
widgets = { cooliowidget = "A really good widget that should be respected" }
}
accountDB['unrelated-account'] = {
name = "John Mysterio's Vault of Wonders",
widgets = { yes = "You buy widget, yes?", widget = "Widget is good!", flubber = "Green, Ready to Rock" }
}
account = accountDB[params.account_id]
res = empty_response()
res:set_header('Content-Type', 'application/json')
print("[WIDGETS] Looking for:", params.account_id, params.widget_id)
if not (account == nil) then
print("[WIDGETS] Found", account.name)
local widget = account.widgets[params.widget_id]
if not (widget == nil) then
res:set_status(200)
res:set_body(json_encode({ widget = widget }))
else
res:set_status(404)
res:set_body(json_encode({ message = "Could not find widget" }))
end
else
res:set_status(404)
res:set_body(json_encode({ message = "Could not find account" }))
end
return res
...@@ -2,16 +2,19 @@ field_handling: Log ...@@ -2,16 +2,19 @@ field_handling: Log
file_handling: File file_handling: File
server: server:
port: 9000 port: 9000
requests: routes:
- /users/@user_id : scripts/get_user_by_id.dyon - route: /users/@user_id
- /users : script: get_user.lua
- route: /accounts/@account_id/widgets/@widget_id
script: accounts/handle_widgets.lua
- route: /users
response:
failure_rate: 5 failure_rate: 5
response_headers: headers:
x-rate-limit: 100 x-rate-limit: 100
x-rate-remaining: 96 x-rate-remaining: 96
x-rate-reset: 10/12/1990 x-rate-reset: 10/12/1990
response_type: application/json body:
response_body:
count: 2 count: 2
data: data:
- id: 1 - id: 1
......
print("We're going to the races");
foo = 1 + 1;
r = empty_response();
r:set_status(200);
r:set_header("X-Powered-By", "Swerve");
r:set_header("Content-Type", "text/plain");
r:set_body("This is my only response");
--return r
print(params.user_id)
r = response(200, "application/json", json_encode({ user_id = params.user_id, path = params.script_path }));
--
--r:unset_body();
--r:set_status(204);
return r
-- return response(200, "application/json", '{ "foo": ' .. foo .. ' }')
fn handle(foo: f64) -> {
println(foo)
{ foo: foo, bar: 123 }
}
\ No newline at end of file
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
<body> <body>
<h1>It's Swervin' Time</h1> <h1>It's Swervin' Time</h1>
<p>This page is part of the swerve example, and includes a stylesheet and stuff.</p> <p>This page is part of the swerve example, and includes a stylesheet and stuff.</p>
<img src="/files/adorable-puppy.jpg">
<script async src="js/say_hello.js"></script> <script async src="js/say_hello.js"></script>
</body> </body>
</html> </html>
\ No newline at end of file
...@@ -7,6 +7,7 @@ use std::default::Default; ...@@ -7,6 +7,7 @@ use std::default::Default;
use serde::{Deserialize, Deserializer, de}; use serde::{Deserialize, Deserializer, de};
use std::fmt; use std::fmt;
use serde_yaml as yaml; use serde_yaml as yaml;
use cli;
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub enum HandlerMethod { pub enum HandlerMethod {
...@@ -52,6 +53,8 @@ pub struct SwerveConfig { ...@@ -52,6 +53,8 @@ pub struct SwerveConfig {
pub file_handling: HandlerMethod, pub file_handling: HandlerMethod,
#[serde(default)] #[serde(default)]
pub server: ServerOptions, pub server: ServerOptions,
#[serde(default="get_empty_routes")]
pub routes: Vec<cli::RouteHandler>,
} }
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
...@@ -74,12 +77,15 @@ fn get_default_address() -> String { String::from("localhost") } ...@@ -74,12 +77,15 @@ fn get_default_address() -> String { String::from("localhost") }
fn get_default_quiet_attr() -> bool { false } fn get_default_quiet_attr() -> bool { false }
fn get_default_index_attr() -> bool { false } fn get_default_index_attr() -> bool { false }
fn get_empty_routes() -> Vec<cli::RouteHandler> { vec![] }
impl Default for SwerveConfig { impl Default for SwerveConfig {
fn default() -> Self { fn default() -> Self {
SwerveConfig { SwerveConfig {
field_handling: HandlerMethod::Log, field_handling: HandlerMethod::Log,
file_handling: HandlerMethod::Log, file_handling: HandlerMethod::Log,
server: ServerOptions::default(), server: ServerOptions::default(),
routes: get_empty_routes(),
} }
} }
} }
......
use std::collections::HashMap;
#[derive(Clone, Debug, Deserialize)]
pub struct RouteHandler {
pub route: String,
pub response: Option<ResponseHandler>,
pub script: Option<String>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct ResponseHandler {
failure_rate: Option<u32>,
#[serde(default="get_default_headers")]
headers: HashMap<String, String>,
}
fn get_default_headers() -> HashMap<String, String> { HashMap::new() }
mod cli; mod cli;
mod config_file; mod config_file;
mod config_routes;
pub mod gpl; pub mod gpl;
pub use self::cli::{Args, USAGE}; pub use self::cli::{Args, USAGE};
pub use self::config_file::{HandlerMethod, SwerveConfig}; pub use self::config_file::{HandlerMethod, SwerveConfig};
\ No newline at end of file pub use self::config_routes::RouteHandler;
\ No newline at end of file
...@@ -4,12 +4,12 @@ ...@@ -4,12 +4,12 @@
extern crate serde; extern crate serde;
#[macro_use] extern crate serde_derive; #[macro_use] extern crate serde_derive;
extern crate serde_yaml; extern crate serde_yaml;
extern crate rhai;
extern crate rocket; extern crate rocket;
extern crate rocket_contrib; extern crate rocket_contrib;
extern crate formdata; extern crate formdata;
extern crate hyper; extern crate hyper;
extern crate rand; extern crate rand;
extern crate rlua;
pub mod cli; pub mod cli;
pub mod routing; pub mod routing;
......
...@@ -6,7 +6,6 @@ extern crate alloc_system; ...@@ -6,7 +6,6 @@ extern crate alloc_system;
extern crate rocket; extern crate rocket;
extern crate docopt; extern crate docopt;
extern crate swerve; extern crate swerve;
extern crate rhai;
use std::process; use std::process;
use docopt::Docopt; use docopt::Docopt;
......
use rocket::{Data, State}; use rocket::{Data, State};
use formdata::{read_formdata, FilePart}; use formdata::{read_formdata, FilePart};
use routing::request::ConvertedHeaders; use routing::request::ConvertedHeaders;
use hyper::header::{Headers, ContentDisposition, DispositionParam}; use hyper::header::{ContentDisposition, DispositionParam};
use rocket::request::FromRequest; use std::io::{copy};
use std::io::{Read, Write, copy};
use std::io::{BufReader, BufWriter};
use std::fs::{OpenOptions, File, create_dir}; use std::fs::{OpenOptions, File, create_dir};
use cli::{HandlerMethod, SwerveConfig}; use cli::{HandlerMethod, SwerveConfig};
use std::path::{Path, PathBuf}; use std::path::{PathBuf};
use std::collections::HashMap; use std::collections::HashMap;
use rand::{Rng, StdRng};
#[post(path = "/", data = "<upload>")] #[post(path = "/", data = "<upload>")]
pub fn to_file(headers: ConvertedHeaders, conf: State<SwerveConfig>, upload: Data) -> Result<String, String> { pub fn to_file(headers: ConvertedHeaders, conf: State<SwerveConfig>, upload: Data) -> Result<String, String> {
...@@ -21,7 +18,12 @@ pub fn to_file(headers: ConvertedHeaders, conf: State<SwerveConfig>, upload: Dat ...@@ -21,7 +18,12 @@ pub fn to_file(headers: ConvertedHeaders, conf: State<SwerveConfig>, upload: Dat
HandlerMethod::File => println!("{:?}", fields), HandlerMethod::File => println!("{:?}", fields),
} }
create_dir("uploads"); match create_dir("uploads") {
Ok(_) => {},
Err(err) => {
return Err(format!("Could not create uploads directory:\n{}", err));
}
}
for file in data.files { for file in data.files {
match conf.file_handling { match conf.file_handling {
...@@ -49,17 +51,17 @@ type Upload = (String, FilePart); ...@@ -49,17 +51,17 @@ type Upload = (String, FilePart);
fn upload_file(file: Upload) { fn upload_file(file: Upload) {
let (name, value) = file; let (name, value) = file;
let content_disposition = value.headers.get::<ContentDisposition>().unwrap(); let content_disposition = value.headers.get::<ContentDisposition>().unwrap();
let file_name = filename_from_disposition(content_disposition); let file_name = filename_from_disposition(content_disposition).unwrap_or(name);
let mut input = File::open(value.path.clone()).unwrap(); let mut input = File::open(value.path.clone()).unwrap();
let mut output = OpenOptions::new() let mut output = OpenOptions::new()
.write(true) .write(true)
.create(true) .create(true)
.open(PathBuf::from("uploads").join(file_name.clone().unwrap_or(String::from("upload_data")))) .open(PathBuf::from("uploads").join(file_name.clone()))
.unwrap(); .unwrap();
copy(&mut input, &mut output).unwrap(); copy(&mut input, &mut output).unwrap();
println!("File written to {}", file_name.unwrap()); println!("File written to {}", file_name);
} }
fn log_file(file: Upload) { fn log_file(file: Upload) {
......
...@@ -2,3 +2,7 @@ pub mod mock_upload; ...@@ -2,3 +2,7 @@ pub mod mock_upload;
pub mod request; pub mod request;
pub mod scripting; pub mod scripting;
pub mod core; pub mod core;
mod request_rewriter;
pub use self::request_rewriter::{ScriptMap, RedirectScripts};
...@@ -57,3 +57,75 @@ impl rocket::response::Responder<'static> for TypedFile { ...@@ -57,3 +57,75 @@ impl rocket::response::Responder<'static> for TypedFile {
Ok(response) Ok(response)
} }
} }
pub struct RequestPath(pub String);
impl <'a, 'req>FromRequest<'a, 'req> for RequestPath {
type Error = ();
fn from_request(request: &'a Request<'req>) -> Outcome<Self, (http::Status, ()), ()> {
let uri = request.uri();
Outcome::Success(RequestPath(String::from(uri.path())))
}
}
pub mod path {
use std::path as std_path;
use std::option::Option::*;
#[derive(PartialEq, Eq, Hash)]
pub struct MatchablePath(pub String);
pub type PathMatch = (String, String);
pub type MatchList = Vec<PathMatch>;
pub type MatchResult = Option<MatchList>;
impl MatchablePath {
pub fn from<T>(src: T) -> Self where T: ToString {
MatchablePath(src.to_string())
}
fn path_to_vec<T: ToString>(string: T) -> Vec<String> {
std_path::PathBuf::from(string.to_string())
.components()
.filter_map(|c| match c {
std_path::Component::Normal(cmp) => cmp.to_str()
.and_then(|s| Some(String::from(s))),
_ => None,
})
.collect()
}
pub fn matches<T>(&self, other: T) -> MatchResult where T: ToString {
let this_path = MatchablePath::path_to_vec(&self.0);
let other_path = MatchablePath::path_to_vec(other);
if this_path.len() == other_path.len() {
let parts_match = this_path.iter()
.zip(other_path.iter())
.fold(true, |acc, (this, other)| {
acc && (this.starts_with("@") || this == other)
});
if parts_match {
Some(this_path.iter()
.zip(other_path.iter())
.filter_map(|(this, other)| {
if this.starts_with("@") {
Some((
this.chars().skip(1).collect::<String>(),
other.clone()
))
} else {
None
}
})
.collect()
)
} else {
None
}
} else {
None
}
}
}
}
use rocket::fairing::{Fairing, Info, Kind};
use rocket::{Request, Data, State};
use cli::SwerveConfig;
use std::collections::HashMap;
use routing::request::path::MatchablePath;
pub struct ScriptMap(pub HashMap<MatchablePath, String>);
impl ScriptMap {
pub fn from_config(conf: &SwerveConfig) -> Self {
let mut map: HashMap<MatchablePath, String> = HashMap::new();
for handler in &conf.routes {
if let Some(ref script) = handler.script {
map.insert(MatchablePath::from(handler.route.clone()), script.clone());
}
}
ScriptMap(map)
}
}
pub struct RedirectScripts;
impl Fairing for RedirectScripts {
fn info(&self) -> Info {
Info {
name: "Redirect Scripts To Handler",
kind: Kind::Request,
}
}
fn on_request(&self, request: &mut Request, _: &Data) {
let script_map_container = request.guard::<State<ScriptMap>>().unwrap();
let script_map: &HashMap<MatchablePath, String> = &script_map_container.0;
for path in script_map.keys() {
let matches = path.matches(request.uri().path());
if let Some(values) = matches {
let script_name = script_map.get(path).unwrap();
let mut value_buffer = String::new();
value_buffer.push_str("script_path=");
value_buffer.push_str(script_name);
for (ref param, ref val) in values {
value_buffer.push_str("&");
value_buffer.push_str(param);
value_buffer.push_str("=");
value_buffer.push_str(val);
}
let path = format!("/__run_script__/?{}", value_buffer);
request.set_uri(path);
break;
}
}
}
}
\ No newline at end of file
use scripting::run_script; use server::LuaRuntime;
use std::path::PathBuf; use rlua::{Lua};
use scripting::{run_script, ScriptResponse};
#[get("/__testing__/run-script")]
pub fn route_script() -> String { use rocket::request::{FromForm, FormItems};
let path = PathBuf::from("example/.swerve/get_user_by_id.rhai"); use std::collections::HashMap;
run_script(path).unwrap_or(String::from("No script"))
} #[derive(Debug)]
\ No newline at end of file pub struct ScriptParams {
pub script_name: String,
pub script_params: HashMap<String, String>,
}
impl <'form> FromForm<'form> for ScriptParams {
type Error = ();
fn from_form(items: &mut FormItems<'form>, _: bool) -> Result<ScriptParams, Self::Error> {
let mut script_name: Option<String> = None;
let mut script_params: HashMap<String, String> = HashMap::new();
for (key, value) in items {
match key.as_str() {
"script_path" if script_name.is_none() => { script_name = Some(String::from(value.as_str())); },
_ => { script_params.insert(String::from(key.as_str()), String::from(value.as_str())); },
};
}
match script_name {
Some(name) => Ok(ScriptParams { script_name: name, script_params }),
None => Err(()),
}
}
}
#[get("/__run_script__?<params>")]
pub fn route_script(params: ScriptParams, runtime: LuaRuntime) -> ScriptResponse {
let lua: Lua = runtime.into();
run_script(format!("example/.swerve/{}", params.script_name), &lua, params.script_params).unwrap_or_else(|| ScriptResponse::default())
}
pub mod script_request;
mod run_script; mod run_script;
mod script_response;
pub use self::run_script::run_script; pub use self::run_script::run_script;
\ No newline at end of file pub use self::script_response::ScriptResponse;
\ No newline at end of file
//use dyon::{Runtime,Module,load_str,Variable,Dfn,Type};
//use dyon::ast::convert;
use std::convert::AsRef; use std::convert::AsRef;
use std::path::Path; use std::path::Path;
use std::fs::File; use std::fs::File;
use std::sync::Arc;
use std::io::Read; use std::io::Read;
use rlua::Lua;
use scripting::ScriptResponse;
use std::collections::HashMap; use std::collections::HashMap;
const SCRIPT_FOOTER: &'static str = " pub fn run_script<P: AsRef<Path>>(path: P, lua: &Lua, params: HashMap<String, String>) -> Option<ScriptResponse> {
fn main() { let mut file = File::open(&path).unwrap();
println(\"Don't directly execute the module, nimrod\") let mut buf = String::new();
}";
match file.read_to_string(&mut buf) {
Ok(_) => {},
Err(_) => return None,
}
pub fn run_script<P: AsRef<Path>>(path: P) -> Option<String> { let file_name = path.as_ref()
// let mut resolution: Option<Variable> = None; .file_name()
// dyon_fn!(fn resolve(val: Variable) { .and_then(|name| name.to_str());
// resolution = Some(val); // if let Ok(val) = runtime.pop() { Some(val) } else { None };
// });
//
// println!("{:?}", resolution);
let mut file = File::open(&path).unwrap(); let params_table = serialize_table("params", params);
let mut buf = String::new();
lua.eval::<ScriptResponse>(&format!("{}\n{}", params_table, buf), file_name)
.map_err(|e| println!("{}", e))
.ok()
}
fn serialize_table(name: &'static str, table: HashMap<String, String>) -> String {
let mut output = format!("local {} = {{", name);
let contents = table.iter()
.fold(
String::new(),
|acc, (key, value)| format!("{} {} = \"{}\",", acc, key, value)
);
output.push_str(&contents[0..contents.len() - 1]);
output.push_str(" }");
file.read_to_string(&mut buf); output
buf.push_str(SCRIPT_FOOTER); }
// let mut script_module = Module::new();
//
// {
// load_str(path.as_ref().to_str().unwrap(), Arc::new(buf), &mut script_module);
// }
Some(buf)
// script_module.add(Arc::new("resolve".into()), resolve, Dfn {
// lts: vec![],
// tys: vec![Type::Object],
// ret: Type::Void,
// });
//
// let mut hashmap: HashMap<Arc<String>, Variable> = HashMap::new();
// runtime.call_str("handle", &[Variable::f64(123f64)], &Arc::new(script_module));
// None
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment