diff --git a/.gitignore b/.gitignore index 9d89521..b84b594 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ node_modules/ build/ dist/ .vscode/tasks.json +examples/test.eve diff --git a/Cargo.toml b/Cargo.toml index 0b4968a..9924498 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,5 +28,11 @@ unicode-segmentation = "1.1.0" iron = "0.5" staticfile = "*" mount = "*" +hyper = "0.11.2" +hyper-tls = "0.1.2" +futures = "0.1.14" +tokio-core = "0.1.9" +data-encoding = "1.2.0" +urlencoding = "1.0.0" natord = "1.0.9" notify = "4.0.0" diff --git a/examples/json-demo.eve b/examples/json-demo.eve new file mode 100644 index 0000000..c2fce1d --- /dev/null +++ b/examples/json-demo.eve @@ -0,0 +1,159 @@ +# JSON Testing + +## Test Encoding + +commit + corey = [#person name: "Corey" | age: 31] + giselle = [#cat name: "Giselle" | age: 7] + twylah = [#cat name: "Twylah" | age: 7] + rachel = [#person name: "Rachel" | age: 28 cats: (giselle, twylah) husband: corey] +end + +commit + [#system/timer resolution: 1000] +end + +search + p = [#person name: "Rachel" age] + [#system/timer second] +bind + p.time += second +end + +search + p = [#person name: "Rachel"] +bind + [#json/encode record: p] +end + +commit + [#ui/button #change-record sort: -1 text: "Change"] + [#ui/button #add-record sort: -1 text: "Add"] +end + +search + [#html/event/click element: [#change-record]] + p = [#person name: "Rachel"] +commit + p.age := 29 +end + +search + [#html/event/click element: [#add-record]] + p = [#person name: "Rachel"] +commit + p.foo := "Hello" +end + + + +search + [#json/encode json-string] +bind + [#html/div text: "Encoded JSON: {{json-string}}"] +end + +## Test Decode + +Decode the record we just encoded + +//search + //[#json/encode json-string] +//commit + //[#json/decode json: json-string] +//end + +//search + //[#json/decode json-object] +//commit + //[#html/div text: "Decoded JSON: {{json-object.name}} is {{json-object.age}} years old"] +//end + + +//commit + //[#ui/button #add-record text: "Add to record"] + //[#ui/button #remove-record text: "Remove From Record"] +//end + +## Debug Display + +search + [#json/encode] + [#json/encode/record record json-string] + output = [#output] +bind + output.children += [#html/div sort: json-string text: "Target: {{json-string}}"] +end + +search + [#json/encode] + [#json/encode/sub-target record json-string] + output = [#output] +bind + output.children += [#html/div text: "Sub: {{json-string}}"] +end + +search + [#json/encode] + [#completed/target record json-string] + output = [#application] +bind + output.children += [#html/div text: "Completed {{json-string}}"] +end + +search + [#json/encode] + encode = [#encode-eavs] + eav = [#json/encode/eav record attribute value] + entity? = if eav = [#json/encode/entity] then "entity" + else "" +bind + encode <- [children: + [#html/div sort: 0 text: "Encode EAVs"] + [#html/table #eav-table | children: + [#html/tr #eav-row eav children: + [#html/td sort:1 text: record] + [#html/td sort:2 text: attribute] + [#html/td sort:3 text: value] + [#html/td sort:4 text: entity?] + ] + ] + + ] +end + +search + [#json/encode] + encode = [#flatten] + [#json/encode/flatten record] +bind + encode <- [children: + [#html/div sort: 0 text: "Flatten"] + [#html/div sort: 1 text: "{{record}}"] + ] +end + +search + [#json/encode] +commit + [#ui/column #application | children: + [#ui/column #output | children: + [#ui/button #next sort: -1 text: "Next"] + ] + [#ui/row #debug | children: + [#ui/column #encode-eavs] + [#ui/column #flatten] + ] + ] +end + +commit + [#html/style text: " + td {padding: 10px;} + table {margin: 10px;} + .ui/column {padding: 10px;} + .ui/row {padding: 10px;} + .output {padding: 20px;} + .encode-eavs {min-width: 350px;} + "] +end \ No newline at end of file diff --git a/libraries/html/html.eve b/libraries/html/html.eve index 033f365..511d921 100644 --- a/libraries/html/html.eve +++ b/libraries/html/html.eve @@ -17,6 +17,12 @@ commit "li" "ul" "ol" + "audio" + "source" + "video" + "table" + "tr" + "td" )] end ~~~ @@ -373,3 +379,11 @@ watch client/websocket ("html/export triggers" element trigger) end ~~~ + +Redirect to a url. +~~~ eve +search + [#html/redirect url] +watch client/websocket + ("html/redirect" url) +end \ No newline at end of file diff --git a/libraries/html/html.ts b/libraries/html/html.ts index e54dc1d..a70b07a 100644 --- a/libraries/html/html.ts +++ b/libraries/html/html.ts @@ -1,6 +1,7 @@ import md5 from "md5"; import "setimmediate"; import {Program, Library, createId, RawValue, RawEAV, RawMap, handleTuples} from "../../ts"; +import url from "url"; const EMPTY:never[] = []; @@ -99,23 +100,27 @@ export class HTML extends Library { this._dummy = document.createElement("div"); window.addEventListener("resize", this._resizeEventHandler("resize-window")); + + // Mouse events window.addEventListener("click", this._mouseEventHandler("click")); window.addEventListener("dblclick", this._mouseEventHandler("double-click")); window.addEventListener("mousedown", this._mouseEventHandler("mouse-down")); window.addEventListener("mouseup", this._mouseEventHandler("mouse-up")); window.addEventListener("contextmenu", this._captureContextMenuHandler()); + document.body.addEventListener("mouseenter", this._hoverEventHandler("hover-in"), true); + document.body.addEventListener("mouseleave", this._hoverEventHandler("hover-out"), true); + // Form events window.addEventListener("change", this._changeEventHandler("change")); window.addEventListener("input", this._inputEventHandler("change")); - window.addEventListener("keydown", this._keyEventHandler("key-down")); - window.addEventListener("keyup", this._keyEventHandler("key-up")); window.addEventListener("focus", this._focusEventHandler("focus"), true); window.addEventListener("blur", this._focusEventHandler("blur"), true); - document.body.addEventListener("mouseenter", this._hoverEventHandler("hover-in"), true); - document.body.addEventListener("mouseleave", this._hoverEventHandler("hover-out"), true); + // Keyboard events + window.addEventListener("keydown", this._keyEventHandler("key-down")); + window.addEventListener("keyup", this._keyEventHandler("key-up")); - // window.addEventListener("hashchange", this._hashChangeHandler("url-change")); + this.getURL(window.location); } protected decorate(elem:Element, elemId:RawValue): Instance { @@ -397,6 +402,11 @@ export class HTML extends Library { if(!instance.__capturedKeys) instance.__capturedKeys = {[code]: true}; else instance.__capturedKeys[code] = true; } + }), + "redirect": handleTuples(({adds, removes}) => { + for(let [url] of adds || EMPTY) { + window.location.replace(`${url}`); + } }) }; @@ -631,6 +641,28 @@ export class HTML extends Library { if(eavs.length) this._sendEvent(eavs); }; } + + getURL(location: Location) { + let {hash, host, hostname, href, pathname, port, protocol, search} = location; + let eavs:RawEAV[] = []; + let urlId = createId(); + eavs.push( + [urlId, "tag", "html/url"], + [urlId, "host", `${host}`], + [urlId, "hostname", `${hostname}`], + [urlId, "href", `${href}`], + [urlId, "pathname", `${pathname}`], + [urlId, "port", `${port}`], + [urlId, "protocol", `${protocol}`], + ); + if(hash !== "") { + eavs.push([urlId, "hash", `${hash.substring(1)}`]); + } + if(search !== "") { + eavs.push([urlId, "query", `${search.substring(1)}`]); + } + this._sendEvent(eavs); + } } Library.register(HTML.id, HTML); diff --git a/libraries/http/http.eve b/libraries/http/http.eve new file mode 100644 index 0000000..f2572f8 --- /dev/null +++ b/libraries/http/http.eve @@ -0,0 +1,136 @@ +# HTTP + +## Send an HTTP Request + +HTTP requests accept several attributes: + +- address - the destination for the request +- method - the method of the request is one of GET, PUT, POST, DELETE, HEAD, TRACE, CONNECT, PATCH +- body - the body of the HTTP request +- headers - request headers take the form of `[#http/header key value]` + +search + request = [#http/request address method body headers: [#http/header key value]] +watch http + ("request", request, address, method, body, key, value) +end + +Default method + +search + request = [#http/request] + not(request.method) +bind + request.method += "GET" +end + +Default empty body + +search + request = [#http/request] + not(request.body) +commit + request.body := "" +end + +Default empty header + +search + request = [#http/request] + not(request.headers) +commit + request.headers := [#http/header key: "" value: ""] +end + +Associate response with its request + +search + response-change = [#http/response/change response] + response = [#html/response request] + request = [#http/request] +commit + response-change := none + request.response := response +end + +Associate an error with its request + +search + error = [#http/request/error request] + request = [#http/request] +commit + request.error := error +end + +Tag a finished request as such + +search + [#http/request/finished request] +commit + request += #finished +end + +Reconstruct a body from chunks, only after the request is`#finished` + +search + [#http/body-chunk response chunk index] + response = [#http/response request] + request = [#http/request #finished] +watch http + ("body", response, chunk, index) +end + +When the full body is reconstructed, attach it to the response + +search + q = [#http/full-body body response] +commit + response.body := body +end + +Clean up body chunks once the body is reconstructed + +search + chunk = [#http/body-chunk response] + response = [#http/response body] +commit + chunk := none +end + +## Receive HTTP Requests + +search + server = [#http/server address] +watch http + ("server", server, address) +end + + +## Parse Query Strings + +search + url = [#html/url query] +bind + [#html/url/parse-query url query] +end + +search + parse = [#html/url/parse-query query] + (pair, i) = string/split[text: query by: "&"] + (token, index) = string/split[text: pair by: "="] +bind + [#html/url/query-kvs parse pair token index] +end + +search + [#html/url/query-kvs parse pair, token: key, index: 1] + [#html/url/query-kvs parse pair, token: value, index: 2] +bind + parse.result += [#html/url/query key value] +end + +search + [#html/url/parse-query url result] +bind + url.parsed-query += result +end diff --git a/libraries/index.ts b/libraries/index.ts index 0de5c24..07d0695 100644 --- a/libraries/index.ts +++ b/libraries/index.ts @@ -1,5 +1,6 @@ export {HTML} from "./html/html"; export {Canvas} from "./canvas/canvas"; export {Console} from "./console/console"; +export {Stream} from "./html/stream"; export {EveCodeMirror} from "./codemirror/codemirror"; export {EveGraph} from "./graph/graph"; diff --git a/libraries/json/json.eve b/libraries/json/json.eve new file mode 100644 index 0000000..bb1ed2c --- /dev/null +++ b/libraries/json/json.eve @@ -0,0 +1,161 @@ +# JSON + +A library for encoding and decoding Eve records into and from JSON + +## Encoding + +Encoding a record is kicked off with `#json/encode`. It creates two records of consequence: + +- `#json/encode/record`: handles encoding records into json. This one is tagged `#json/encode/target-record`, which means it is flagged for output. +- `#json/encode/flatten`: flattens a record into a/v pairs + +search + [#json/encode record] +bind + [#json/encode/record #json/encode/target-record record] + [#json/encode/flatten record] +end + +`#json/encode/record` are given a starting point for JSON + +search + target = [#json/encode/record record] + not(target.json-string) +commit + target.json-string := "{ " +end + +We flatten records with lookup. + +search + [#json/encode/flatten record] + lookup[entity: record attribute value] +bind + encode-eav = [#json/encode/eav record attribute value] +end + +sub-records are marked `#json/encode/entity` + +search + encode = [#json/encode/eav record attribute value] + lookup[entity: value] +bind + encode += #json/encode/entity +end + +### Encode A/V Pairs + +We can join all non entities in a json encoded string + +search + eav = [#json/encode/eav record attribute value] + not(eav = [#json/encode/entity]) +bind + [#json/encode/entity/av-pair record av: "\"{{attribute}}\": \"{{value}}\""] +end + +search + [#json/encode/entity/av-pair record av] +bind + [#string/join #json/encode/join-avs record with: ", " | strings: av] +end + +search + [#json/encode/join-avs record result] +bind + [#json/encode/complete-av record json-string: result] +end + +### Encode Sub records + +`#json/encode/entity` records can be encoded just like the target record. + +search + [#json/encode/eav record attribute value #json/encode/entity] +bind + [#json/encode/record record: value] + [#json/encode/flatten record: value] +end + +Join eavs into a json object + +search + encode = [#json/encode/eav #json/encode/entity attribute] + finished = [#json/encode/finished] + encode.value = finished.record +bind + [#string/join #json/encode/join-object parent: encode.record attribute record: "{{encode.record}}|{{attribute}}" with: ", " | strings: finished.json-string ] +end + +Put all json object strings into an array form. attaching its attribute + +search + [#json/encode/join-object result parent attribute] +bind + [#json/encode/complete-av record: parent json-string: "\"{{attribute}}\": [ {{result}} ]"] +end + +### Bring it all together + +Join all encoded avs into a complete string + +search + complete-av = [#json/encode/complete-av record] + target-record = [#json/encode/record record] +bind + [#string/join #json/encode/join-complete record with: ", " | strings: complete-av.json-string] +end + +Ensconce finished records with curly braces + +search + [#json/encode/join-complete record result] +bind + [#json/encode/finished record json-string: "{ {{result}} }"] +end + +When the full target record is encoded, hang it on the orginal `#json-encode` record + +search + [#json/encode/finished record json-string] + encode = [#json/encode record] + [#json/encode/target-record record] +bind + encode.json-string += json-string +end + +### Joining Strings in Eve + +search + join = [#string/join strings with] +watch json + ("join", join, strings, with) +end + +Put the joined string in the original `#string/join` record, and get rid of the result + +search + join-result = [#string/join/result result] + join = [#string/join strings with] + join-result.record = join +commit + join-result := none + join.result := result +end + +## Decoding + +search + decode = [#json/decode json] +watch json + ("decode", decode, json) +end + +A decoded string comes through on a change + +search + decode-change = [#json/decode/change decode json-object] +commit + decode.json-object := json-object + decode-change := none +end diff --git a/src/bin/main.rs b/src/bin/main.rs index bf1e214..e8d0f48 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -11,6 +11,8 @@ use eve::ops::{DebugMode, ProgramRunner, Persister}; use eve::watchers::system::{SystemTimerWatcher, PanicWatcher}; use eve::watchers::console::{ConsoleWatcher, PrintDiffWatcher}; use eve::watchers::file::FileWatcher; +use eve::watchers::json::JsonWatcher; +use eve::watchers::http::HttpWatcher; //------------------------------------------------------------------------- // Main @@ -68,6 +70,8 @@ fn main() { runner.program.attach(Box::new(FileWatcher::new(outgoing.clone()))); runner.program.attach(Box::new(ConsoleWatcher::new())); runner.program.attach(Box::new(PrintDiffWatcher::new())); + runner.program.attach(Box::new(JsonWatcher::new(outgoing.clone()))); + runner.program.attach(Box::new(HttpWatcher::new(outgoing.clone()))); runner.program.attach(Box::new(PanicWatcher::new())); } diff --git a/src/bin/server.rs b/src/bin/server.rs index 6eefe50..a52b296 100644 --- a/src/bin/server.rs +++ b/src/bin/server.rs @@ -28,12 +28,14 @@ use eve::paths::EvePaths; use eve::ops::{ProgramRunner, RunLoop, RunLoopMessage, RawChange, Internable, Persister, JSONInternable}; use eve::watchers::system::{SystemTimerWatcher, PanicWatcher}; use eve::watchers::compiler::{CompilerWatcher}; +use eve::watchers::http::{HttpWatcher}; use eve::watchers::textcompiler::{RawTextCompilerWatcher}; use eve::watchers::console::{ConsoleWatcher}; use eve::watchers::file::{FileWatcher}; use eve::watchers::editor::EditorWatcher; use eve::watchers::remote::{Router, RouterMessage, RemoteWatcher}; use eve::watchers::websocket::WebsocketClientWatcher; +use eve::watchers::json::{JsonWatcher}; extern crate iron; extern crate staticfile; @@ -78,12 +80,15 @@ impl ClientHandler { router.lock().expect("ERROR: Failed to lock router: Cannot register new client.").register(&client_name, outgoing.clone()); if !eve_flags.clean { runner.program.attach(Box::new(SystemTimerWatcher::new(outgoing.clone()))); + runner.program.attach(Box::new(JsonWatcher::new(outgoing.clone()))); + runner.program.attach(Box::new(HttpWatcher::new(outgoing.clone()))); runner.program.attach(Box::new(CompilerWatcher::new(outgoing.clone(), false))); runner.program.attach(Box::new(RawTextCompilerWatcher::new(outgoing.clone()))); runner.program.attach(Box::new(FileWatcher::new(outgoing.clone()))); runner.program.attach(Box::new(WebsocketClientWatcher::new(out.clone(), client_name))); runner.program.attach(Box::new(ConsoleWatcher::new())); runner.program.attach(Box::new(PanicWatcher::new())); + runner.program.attach(Box::new(JsonWatcher::new(outgoing.clone()))); runner.program.attach(Box::new(RemoteWatcher::new(client_name, &router.lock().expect("ERROR: Failed to lock router: Cannot init RemoteWatcher.").deref()))); if eve_flags.editor { let editor_watcher = EditorWatcher::new(&mut runner, router.clone(), out.clone(), eve_paths.libraries(), eve_paths.programs()); diff --git a/src/compiler.rs b/src/compiler.rs index 8ffc961..785ecb9 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -122,6 +122,8 @@ lazy_static! { m.insert("string/replace".to_string(), FunctionInfo::new(vec!["text", "replace", "with"])); m.insert("string/contains".to_string(), FunctionInfo::new(vec!["text", "substring"])); m.insert("string/lowercase".to_string(), FunctionInfo::new(vec!["text"])); + m.insert("string/encode".to_string(), FunctionInfo::new(vec!["text"])); + m.insert("string/url-encode".to_string(), FunctionInfo::new(vec!["text"])); m.insert("string/uppercase".to_string(), FunctionInfo::new(vec!["text"])); m.insert("string/length".to_string(), FunctionInfo::new(vec!["text"])); m.insert("string/substring".to_string(), FunctionInfo::new(vec!["text", "from", "to"])); diff --git a/src/ops.rs b/src/ops.rs index c92859e..c89be96 100644 --- a/src/ops.rs +++ b/src/ops.rs @@ -6,6 +6,8 @@ extern crate time; extern crate serde_json; extern crate bincode; extern crate term_painter; +extern crate data_encoding; +extern crate urlencoding; extern crate natord; use unicode_segmentation::UnicodeSegmentation; @@ -35,6 +37,7 @@ use std::f32::consts::{PI}; use std::mem; use std::usize; use rand::{Rng, SeedableRng, XorShiftRng}; +use self::data_encoding::base64; use self::term_painter::ToStyle; use self::term_painter::Color::*; use parser; @@ -703,6 +706,10 @@ impl Internable { Internable::Number(value) } + pub fn from_str(s: &str) -> Internable { + Internable::String(s.to_string()) + } + pub fn print(&self) -> String { match self { &Internable::String(ref s) => { @@ -1310,6 +1317,8 @@ pub fn make_function(op: &str, params: Vec, output: Field) -> Constraint "string/length" => string_length, "eve/type-of" => eve_type_of, "eve/parse-value" => eve_parse_value, + "string/encode" => string_encode, + "string/url-encode" => string_urlencode, "concat" => concat, "gen_id" => gen_id, _ => panic!("Unknown function: {:?}", op) @@ -1600,6 +1609,23 @@ pub fn string_length(params: Vec<&Internable>) -> Option { } } +pub fn string_encode(params: Vec<&Internable>) -> Option { + match params.as_slice() { + &[&Internable::String(ref text)] => Some(Internable::String(base64::encode(text.as_bytes()))), + &[&Internable::Number(ref number)] => Some(Internable::String(number.to_string())), + _ => None + } +} + +pub fn string_urlencode(params: Vec<&Internable>) -> Option { + match params.as_slice() { + &[&Internable::String(ref text)] => Some(Internable::String(urlencoding::encode(text))), + &[&Internable::Number(ref number)] => Some(Internable::String(number.to_string())), + _ => None + } +} + + pub fn string_substring(params: Vec<&Internable>) -> Option { let params_slice = params.as_slice(); match params_slice { diff --git a/src/parser.rs b/src/parser.rs index fa8ff94..e04b2c1 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -71,7 +71,7 @@ parser!(number(state) -> Node<'a> { whitespace_parser!(escaped_quote(state) -> Node<'a> { tag!(state, "\\"); - let escaped = alt_tag!(state, ["\"" "n" "t"]); + let escaped = alt_tag!(state, ["\"" "\\" "n" "t"]); let ch = match escaped { "n" => "\n", "t" => "\t", @@ -94,7 +94,7 @@ whitespace_parser!(string_bracket(state) -> Node<'a> { }); whitespace_parser!(string_chars(state) -> Node<'a> { - let chars = any_except!(state, "\"{"); + let chars = any_except!(state, "\\\"{"); result!(state, Node::RawString(chars)) }); diff --git a/src/watchers/http.rs b/src/watchers/http.rs new file mode 100644 index 0000000..5d66ab3 --- /dev/null +++ b/src/watchers/http.rs @@ -0,0 +1,205 @@ +use super::super::indexes::{WatchDiff}; +use super::super::ops::{Internable, Interner, RawChange, RunLoopMessage}; +use std::sync::mpsc::{Sender}; +use watchers::json::{new_change}; +use super::Watcher; + +extern crate futures; +extern crate hyper; +extern crate hyper_tls; +extern crate tokio_core; +extern crate serde_json; +extern crate serde; +use self::futures::{Future, Stream}; +use self::hyper::Client; +use self::hyper_tls::HttpsConnector; +use self::tokio_core::reactor::Core; +use self::hyper::{Method}; +use std::thread; +use std::io::{Write}; +extern crate iron; +use self::iron::prelude::*; +use self::iron::{status, url}; +use std::collections::HashMap; + +pub struct HttpWatcher { + name: String, + responses: HashMap>, + outgoing: Sender, +} + +impl HttpWatcher { + pub fn new(outgoing: Sender) -> HttpWatcher { + HttpWatcher { name: "http".to_string(), responses: HashMap::new(), outgoing } + } +} + +impl Watcher for HttpWatcher { + fn get_name(& self) -> String { + self.name.clone() + } + fn set_name(&mut self, name: &str) { + self.name = name.to_string(); + } + fn on_diff(&mut self, interner:&mut Interner, diff:WatchDiff) { + let mut requests: HashMap = HashMap::new(); + for add in diff.adds { + let kind = Internable::to_string(interner.get_value(add[0])); + let id = Internable::to_string(interner.get_value(add[1])); + let address = Internable::to_string(interner.get_value(add[2])); + match &kind[..] { + "request" => { + let body = Internable::to_string(interner.get_value(add[4])); + let key = Internable::to_string(interner.get_value(add[5])); + let value = Internable::to_string(interner.get_value(add[6])); + if !requests.contains_key(&id) { + let url = address.parse::().unwrap(); + let method = Internable::to_string(interner.get_value(add[3])); + let rmethod: Method = match &method.to_lowercase()[..] { + "get" => Method::Get, + "put" => Method::Put, + "post" => Method::Post, + "delete" => Method::Delete, + "head" => Method::Head, + "trace" => Method::Trace, + "connect" => Method::Connect, + "patch" => Method::Patch, + _ => Method::Get + }; + let req = hyper::Request::new(rmethod, url); + requests.insert(id.clone(), req); + } + let req = requests.get_mut(&id).unwrap(); + if key != "" { + req.headers_mut().set_raw(key, vec![value.into_bytes().to_vec()]); + } + if body != "" { + req.set_body(body); + } + }, + "server" => { + http_server(address, &self.outgoing); + }, + "body" => { + let response_id = Internable::to_string(interner.get_value(add[1])); + let chunk = Internable::to_string(interner.get_value(add[2])); + let index = Internable::to_number(interner.get_value(add[3])) as u32; + if self.responses.contains_key(&response_id) { + match self.responses.get_mut(&response_id) { + Some(v) => v.push((index,chunk)), + _ => (), + } + } else { + self.responses.insert(response_id, vec![(index.clone(), chunk.clone())]); + } + } + _ => {}, + } + } + // Send the HTTP request + for (id, request) in requests.drain() { + send_http_request(&id, request, &self.outgoing); + }; + // Reconstruct the body from chunks + for (response_id, mut chunk_vec) in self.responses.drain() { + chunk_vec.sort(); + let body: String = chunk_vec.iter().fold("".to_string(), |acc, ref x| { + let &&(ref ix, ref chunk) = x; + acc + chunk + }); + let full_body_id = format!("http/full-body|{:?}",response_id); + self.outgoing.send(RunLoopMessage::Transaction(vec![ + new_change(&full_body_id, "tag", Internable::from_str("http/full-body"), "http/request"), + new_change(&full_body_id, "body", Internable::String(body), "http/request"), + new_change(&full_body_id, "response", Internable::String(response_id), "http/request"), + ])).unwrap(); + }; + } +} + +fn http_server(address: String, outgoing: &Sender) -> thread::JoinHandle<()> { + thread::spawn(move || { + Iron::new(|req: &mut Request| { + println!("STARTED SERVER"); + let node = "http/server"; + let hostname: String = match req.url.host() { + url::Host::Domain(s) => s.to_string(), + url::Host::Ipv4(s) => s.to_string(), + url::Host::Ipv6(s) => s.to_string(), + }; + let request_id = format!("http/request|{:?}",req.url); + let url_id = format!("http/url|{:?}",request_id); + let mut request_changes: Vec = vec![]; + request_changes.push(new_change(&request_id, "tag", Internable::from_str("http/request"), node)); + request_changes.push(new_change(&request_id, "url", Internable::String(url_id.clone()), node)); + request_changes.push(new_change(&url_id, "tag", Internable::from_str("http/url"), node)); + request_changes.push(new_change(&url_id, "hostname", Internable::String(hostname), node)); + request_changes.push(new_change(&url_id, "port", Internable::String(req.url.port().to_string()), node)); + request_changes.push(new_change(&url_id, "protocol", Internable::from_str(req.url.scheme()), node)); + match req.url.fragment() { + Some(s) => request_changes.push(new_change(&url_id, "hash", Internable::from_str(s), node)), + _ => (), + }; + match req.url.query() { + Some(s) => request_changes.push(new_change(&url_id, "query", Internable::from_str(s), node)), + _ => (), + }; + //outgoing.send(RunLoopMessage::Transaction(request_changes)); + Ok(Response::with((status::Ok, ""))) + }).http(address).unwrap(); + }) +} + +fn send_http_request(id: &String, request: hyper::Request, outgoing: &Sender) { + let node = "http/request"; + let mut core = Core::new().unwrap(); + let handle = core.handle(); + let client = Client::configure() + .connector(HttpsConnector::new(4,&handle).unwrap()) + .build(&handle); + let mut ix: f32 = 1.0; + let work = client.request(request).and_then(|res| { + let mut response_changes: Vec = vec![]; + let status = res.status().as_u16(); + let response_id = format!("http/response|{:?}",id); + let response_change_id = format!("http/response/received|{:?}",id); + response_changes.push(new_change(&response_change_id, "tag", Internable::from_str("http/response/received"), node)); + response_changes.push(new_change(&response_change_id, "response", Internable::String(response_id.clone()), node)); + response_changes.push(new_change(&response_id, "tag", Internable::from_str("http/response"), node)); + response_changes.push(new_change(&response_id, "status", Internable::String(status.to_string()), node)); + response_changes.push(new_change(&response_id, "request", Internable::String(id.clone()), node)); + outgoing.send(RunLoopMessage::Transaction(response_changes)).unwrap(); + res.body().for_each(|chunk| { + let response_id = format!("http/response|{:?}",id); + let chunk_id = format!("body-chunk|{:?}|{:?}",&response_id,ix); + let mut vector: Vec = Vec::new(); + vector.write_all(&chunk).unwrap(); + let body_string = String::from_utf8(vector).unwrap(); + outgoing.send(RunLoopMessage::Transaction(vec![ + new_change(&chunk_id, "tag", Internable::from_str("http/body-chunk"), node), + new_change(&chunk_id, "response", Internable::String(response_id), node), + new_change(&chunk_id, "chunk", Internable::String(body_string), node), + new_change(&chunk_id, "index", Internable::from_number(ix), node) + ])).unwrap(); + ix = ix + 1.0; + Ok(()) + }) + }); + match core.run(work) { + Ok(_) => (), + Err(e) => { + // Form an HTTP Error + let error_id = format!("http/request/error|{:?}",&id); + let mut error_changes: Vec = vec![]; + error_changes.push(new_change(&error_id, "tag", Internable::from_str("http/request/error"), node)); + error_changes.push(new_change(&error_id, "request", Internable::String(id.clone()), node)); + error_changes.push(new_change(&error_id, "error", Internable::String(format!("{:?}",e)), node)); + outgoing.send(RunLoopMessage::Transaction(error_changes)).unwrap(); + }, + } + let finished_id = format!("http/request/finished|{:?}",id); + outgoing.send(RunLoopMessage::Transaction(vec![ + new_change(&finished_id, "tag", Internable::from_str("http/request/finished"), node), + new_change(&finished_id, "request", Internable::from_str(id), node), + ])).unwrap(); +} \ No newline at end of file diff --git a/src/watchers/json.rs b/src/watchers/json.rs new file mode 100644 index 0000000..c10a24e --- /dev/null +++ b/src/watchers/json.rs @@ -0,0 +1,150 @@ +use super::super::indexes::{WatchDiff}; +use super::super::ops::{Internable, Interner, RawChange, RunLoopMessage}; +use std::sync::mpsc::{Sender}; +use super::Watcher; + +extern crate serde_json; +extern crate serde; +use self::serde_json::{Value}; +use std::collections::HashMap; + +pub struct JsonWatcher { + name: String, + outgoing: Sender, + join_strings_map: HashMap, +} + +impl JsonWatcher { + pub fn new(outgoing: Sender) -> JsonWatcher { + JsonWatcher { name: "json".to_string(), join_strings_map: HashMap::new(), outgoing } + } +} + +#[derive(Debug, Clone)] +pub struct JoinStrings { + strings: Vec, + with: String +} + +impl JoinStrings { + pub fn new(with: String) -> JoinStrings { + JoinStrings { with, strings: vec![] } + } + pub fn join(&self) -> String { + self.strings.join(self.with.as_ref()) + } +} + +impl Watcher for JsonWatcher { + fn get_name(& self) -> String { + self.name.clone() + } + fn set_name(&mut self, name: &str) { + self.name = name.to_string(); + } + fn on_diff(&mut self, interner:&mut Interner, diff:WatchDiff) { + let mut changes: Vec = vec![]; + for remove in diff.removes { + let kind = Internable::to_string(interner.get_value(remove[0])); + match kind.as_ref() { + "join" => { + let id = Internable::to_string(interner.get_value(remove[1])); + let string = Internable::to_string(interner.get_value(remove[2])); + let join_strings = self.join_strings_map.get_mut(&id).unwrap(); + let index = join_strings.strings.iter().position(|x| *x == string).unwrap(); + join_strings.strings.remove(index); + }, + _ => {}, + } + } + for add in diff.adds { + let kind = Internable::to_string(interner.get_value(add[0])); + let record_id = Internable::to_string(interner.get_value(add[1])); + match kind.as_ref() { + "decode" => { + let value = Internable::to_string(interner.get_value(add[2])); + let v: Value = serde_json::from_str(&value).unwrap(); let change_id = format!("json/decode/change|{:?}",record_id); + value_to_changes(change_id.as_ref(), "json-object", v, "json/decode", &mut changes); + changes.push(new_change(&change_id, "tag", Internable::from_str("json/decode/change"), "json/decode")); + changes.push(new_change(&change_id, "decode", Internable::String(record_id), "json/decode")); + }, + "join" => { + let id = Internable::to_string(interner.get_value(add[1])); + let string = Internable::to_string(interner.get_value(add[2])); + let with = Internable::to_string(interner.get_value(add[3])); + if self.join_strings_map.contains_key(&id) { + let join_strings = self.join_strings_map.get_mut(&id).unwrap(); + join_strings.strings.push(string); + } else { + let mut join_strings = JoinStrings::new(with); + join_strings.strings.push(string); + self.join_strings_map.insert(id, join_strings); + } + }, + _ => {}, + } + } + + for (record_id, join_strings) in self.join_strings_map.iter() { + let join_id = format!("string/join|{:?}",record_id); + changes.push(new_change(&join_id, "tag", Internable::from_str("string/join/result"), "string/join")); + changes.push(new_change(&join_id, "result", Internable::String(join_strings.join()), "string/join")); + changes.push(new_change(&join_id, "record", Internable::String(record_id.to_owned()), "string/join")); + } + match self.outgoing.send(RunLoopMessage::Transaction(changes)) { + Err(_) => (), + _ => (), + } + } +} + +pub fn new_change(e: &str, a: &str, v: Internable, n: &str) -> RawChange { + RawChange {e: Internable::from_str(e), a: Internable::from_str(a), v: v.clone(), n: Internable::from_str(n), count: 1} +} + +pub fn value_to_changes(id: &str, attribute: &str, value: Value, node: &str, changes: &mut Vec) { + match value { + Value::Number(n) => { + if n.is_u64() { + let v = Internable::from_number(n.as_u64().unwrap() as f32); + changes.push(new_change(id,attribute,v,node)); + } else if n.is_i64() { + let v = Internable::from_number(n.as_i64().unwrap() as f32); + changes.push(new_change(id,attribute,v,node)); + } else if n.is_f64() { + let v = Internable::from_number(n.as_f64().unwrap() as f32); + changes.push(new_change(id,attribute,v,node)); + }; + }, + Value::String(ref n) => { + changes.push(new_change(id,attribute,Internable::String(n.clone()),node)); + }, + Value::Bool(ref n) => { + let b = match n { + &true => "true", + &false => "false", + }; + changes.push(new_change(id,attribute,Internable::from_str(b),node)); + }, + Value::Array(ref n) => { + for (ix, value) in n.iter().enumerate() { + let ix = ix + 1; + let array_id = format!("array|{:?}|{:?}|{:?}", id, ix, value); + let array_id = &array_id[..]; + changes.push(new_change(id,attribute,Internable::from_str(array_id),node)); + changes.push(new_change(array_id,"tag",Internable::from_str("array"),node)); + changes.push(new_change(array_id,"index",Internable::String(ix.to_string()),node)); + value_to_changes(array_id, "value", value.clone(), node, changes); + } + }, + Value::Object(ref n) => { + let object_id = format!("{:?}",n); + changes.push(new_change(id,attribute,Internable::String(object_id.clone()),node)); + changes.push(new_change(id,"tag",Internable::from_str("json-object"),node)); + for key in n.keys() { + value_to_changes(&mut object_id.clone(), key, n[key].clone(), node, changes); + } + }, + _ => {}, + } +} \ No newline at end of file diff --git a/src/watchers/mod.rs b/src/watchers/mod.rs index 96e764d..2a15189 100644 --- a/src/watchers/mod.rs +++ b/src/watchers/mod.rs @@ -11,7 +11,9 @@ pub mod file; pub mod console; pub mod system; pub mod compiler; +pub mod http; +pub mod json; pub mod textcompiler; pub mod editor; pub mod remote; -pub mod websocket; +pub mod websocket; \ No newline at end of file diff --git a/tests/base.rs b/tests/base.rs index 1db9e2f..f65ef85 100644 --- a/tests/base.rs +++ b/tests/base.rs @@ -251,6 +251,38 @@ test!(base_join_nested_record, { end }); +//-------------------------------------------------------------------- +// Strings +//-------------------------------------------------------------------- + +test!(base_string, { + search + baz = "Hello" + bind + [#foo baz] + end + + search + [#foo baz: "Hello"] + bind + [#success] + end +}); + +test!(base_string_escape_chars, { + search + baz = "Hello \\ \n \"World\" " + bind + [#foo baz] + end + + search + [#foo baz: "Hello \\ \n \"World\" "] + bind + [#success] + end +}); + //-------------------------------------------------------------------- // Interpolation //-------------------------------------------------------------------- diff --git a/ts/main.ts b/ts/main.ts index e6fd8d7..d481686 100644 --- a/ts/main.ts +++ b/ts/main.ts @@ -80,6 +80,7 @@ class MultiplexedConnection extends Connection { this.addPane(client, html.getContainer()); program.attach("canvas"); program.attach("console"); + program.attach("stream"); program.attach("code-block"); program.attach("graph"); },