From 028cc115a0fdb054d5a4e8450ef0482a670500b7 Mon Sep 17 00:00:00 2001 From: YetAnotherMinion Date: Wed, 12 Jan 2022 04:56:15 +0000 Subject: [PATCH] feat: PoC for generating webpage with database --- Cargo.toml | 5 + benches/string_replacement.rs | 71 +++++ src/exec/astrid_pages.rs | 486 +++++++++++++++++++++++++++++- src/exec/fixtures/astrid_pages.rs | 218 ++++++++++++++ src/exec/fixtures/mod.rs | 1 + src/exec/mod.rs | 8 +- src/exec/scripting.rs | 6 +- src/reporting.rs | 7 +- 8 files changed, 780 insertions(+), 22 deletions(-) create mode 100644 benches/string_replacement.rs create mode 100644 src/exec/fixtures/astrid_pages.rs diff --git a/Cargo.toml b/Cargo.toml index 8de77d8..a85d59d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,9 @@ home = "0.5" # Required to transpile view functions to Rust genco = "0.15" +# Required to generate fixture Elm files +elm-quote = { path = "../../../infra/rust-elm-quote" } + # All of these are required for deno's javascript runtime. We need to keep the # same versions as other projects in our cargo workspace. Multiple different @@ -45,3 +48,5 @@ serde_v8 = "0.15" sqlx = { version = "0.5", features = [ "sqlite", "macros", "runtime-tokio-rustls", "chrono", "json", "uuid" ] } oneshot = "0.1.3" +[dev-dependencies] +aho-corasick = "0.7" diff --git a/benches/string_replacement.rs b/benches/string_replacement.rs new file mode 100644 index 0000000..4614486 --- /dev/null +++ b/benches/string_replacement.rs @@ -0,0 +1,71 @@ +use aho_corasick::{AhoCorasick, AhoCorasickBuilder}; + +// TODO figure out why this code takes ~70ms to munge the javascript. By comparison just writing a +// bunch of chainged string.replace( , ).replace( , ).replace .... takes about 16ms. + let mut patterns = Vec::new(); + let mut replace_with = Vec::new(); + + patterns.push("'REPLACE_ME_WITH_JSON_STRINGIFY'"); + replace_with.push("JSON.stringify(x)"); + + patterns.push("$elm$json$Json$Decode$fail('REPLACE_ME_WITH_BYTES_DECODER');"); + replace_with.push(r#" + _Json_decodePrim(function(value) { + return (typeof value === 'object' && value instanceof DataView) + ? $elm$core$Result$Ok(value) + : _Json_expecting('a DataView', value); + }); + "#); + + patterns.push(";}(this));"); + replace_with.push(";}(globalThis));"); + + + // let mut final_script = data + + if sqlite_path.is_some() { + patterns.push("var $author$project$Astrid$Query$execute = function (query) {\n\treturn $author$project$Astrid$Query$dummyExecute;\n};"); + replace_with.push(include_str!("fixtures/sql-client-integration.js")); + + patterns.push("var $author$project$Astrid$Query$fetch = F3(\n\tfunction (sql, parameters, decoder) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});"); + replace_with.push("var $author$project$Astrid$Query$fetch = _Query_fetchAll;"); + + patterns.push("var $author$project$Astrid$Query$fetchOne = F3(\n\tfunction (sql, parameters, decoder) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});"); + replace_with.push("var $author$project$Astrid$Query$fetchOne = _Query_fetchOne;"); + + patterns.push("var $author$project$Astrid$Query$map5 = F6(\n\tfunction (f, a, b, c, d, e) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});"); + replace_with.push(r#"var $author$project$Astrid$Query$map5 = _Query_map5;"#); + + patterns.push("var $author$project$Astrid$Query$map4 = F5(\n\tfunction (f, a, b, c, d) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});"); + replace_with.push(r#"var $author$project$Astrid$Query$map4 = _Query_map4;"#); + + patterns.push("var $author$project$Astrid$Query$map3 = F4(\n\tfunction (f, a, b, c) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});"); + replace_with.push(r#"var $author$project$Astrid$Query$map3 = _Query_map3;"#); + + patterns.push("var $author$project$Astrid$Query$map2 = F3(\n\tfunction (f, a, b) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});"); + replace_with.push(r#"var $author$project$Astrid$Query$map2 = _Query_map2;"#); + + patterns.push("var $author$project$Astrid$Query$map = F2(\n\tfunction (f, a) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});"); + replace_with.push(r#"var $author$project$Astrid$Query$map = _Query_map1;"#); + + patterns.push("var $author$project$Astrid$Query$andThen = F2(\n\tfunction (f, q) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});"); + replace_with.push(r#"var $author$project$Astrid$Query$andThen = _Query_andThen;"#); + } + debug_assert!(patterns.len() == replace_with.len()); + + // let mut final_script = Vec::with_capacity(data.len() + 8 * 1024); + + let span = info_span!("build aho-corasick patterns"); + let timing_guard = span.enter(); + //let ac = AhoCorasick::new(&patterns); + let ac = AhoCorasickBuilder::new() + .auto_configure(&patterns) + .build(&patterns); + drop(timing_guard); + let span = info_span!("run replacements"); + let timing_guard = span.enter(); + for _ in ac.find_iter(data.as_bytes()) { + + } + drop(timing_guard); + let mut final_script = ac.replace_all_bytes(data.as_bytes(), &replace_with); diff --git a/src/exec/astrid_pages.rs b/src/exec/astrid_pages.rs index 8da61d7..323ac15 100644 --- a/src/exec/astrid_pages.rs +++ b/src/exec/astrid_pages.rs @@ -1,18 +1,482 @@ -use crate::reporting::{Problem}; -use std::path::{PathBuf}; +use crate::exec::fixtures::astrid_pages::ScriptError; +use crate::exec::{fixtures, runtime}; +use crate::reporting::{CompilerError, InterpreterError, Problem, TypeError}; +use deno_core::futures::StreamExt; +use elm_project_utils::{setup_generator_project, ElmResult}; +use os_pipe::dup_stderr; +use rusty_v8 as v8; +use sqlx::sqlite::SqlitePool; +use sqlx::Row; +use std::cell::RefCell; +use std::convert::TryFrom; +use std::fs; +use std::io; +use std::io::Write; +use std::path::PathBuf; +use std::process::{Command, Stdio}; +use std::sync::Arc; +use std::time::Instant; +use tokio; +use tracing::{info_span, Instrument}; + +#[derive(Debug, Copy, Clone)] +pub enum OutputType { + Html, + String, + Bytes, + Value, +} pub(crate) fn run( - _debug: bool, - _verbosity: u64, - _sqlite_path: Option, - _elm_project_dir: PathBuf, - _source_checksum: u64, - _entrypoint: elmi::Global, - _output: Option, + debug: bool, + verbosity: u64, + sqlite_path: Option, + elm_project_dir: PathBuf, + source_checksum: u64, + entrypoint: elmi::Global, + output: Option, ) -> Result<(), Problem> { // step 6 create our private project - // let generator_dir = setup_generator_project(verbosity, elm_project_dir.clone())?; + let packages = ["ThinkAlexandria/elm-html-in-elm", "elm/json", "elm/bytes"]; + // I added a couple of random bytes to the directory name to reduce the risk of + // collisions with other programs that also use elm-stuff for their scratch space + let our_temp_dir = elm_project_dir.join("elm-stuff").join("starmelon-5d9ecc"); + let generator_dir = + setup_generator_project(verbosity, elm_project_dir.clone(), our_temp_dir, &packages)?; + + // TODO Step 7 find the flags module name + // TODO right now just hard code the module name and require projects to have `module Flags` + // I will actually detect the type and module after I get the first run a simple page proof of + // concept working + let flags_module = "Flags".to_owned(); + + // step 8 create an Elm fixture file to run our function + let span = info_span!("create Elm fixture files"); + let timing_guard = span.enter(); + // TODO actually detect the type of each route's output, and validate they are all something I + // can understand. I will need the interfaces dictionary for this. + let (gen_module_name, source) = fixtures::astrid_pages::generate( + source_checksum, + flags_module, + &[(entrypoint.0.module.clone().to_string(), OutputType::Html)], + ); + + let mut source_filename = generator_dir.join("src").join(&gen_module_name); + source_filename.set_extension("elm"); + + let span = info_span!("file writes"); + let file_write_timing_guard = span.enter(); + fs::File::create(&source_filename) + .map_err(|io_err| CompilerError::WriteOutputFailed(io_err, source_filename.clone()))? + .write_all(source.as_bytes()) + .map_err(|io_err| CompilerError::WriteOutputFailed(io_err, source_filename.clone()))?; + drop(file_write_timing_guard); + drop(timing_guard); + + // step 8 compile the fixture + let mut intermediate_file = generator_dir.join("obj").join(&gen_module_name); + intermediate_file.set_extension("js"); + + let mut command = Command::new("elm"); + + command + .arg("make") + .arg("--output") + .arg(&intermediate_file) + .current_dir(&generator_dir) + .stdin(Stdio::null()) + .stderr(Stdio::piped()); + if verbosity < 1 { + command.stdout(Stdio::piped()); + } else { + let pipe = dup_stderr() + .map_err(|io_err| CompilerError::ReadInputFailed(io_err, "stdout".into()))?; + command.stdout(pipe); + } + + if debug { + command.arg("--debug"); + } + command.arg(&source_filename); + + let span = info_span!("elm make fixture file"); + let timing_guard = span.enter(); + match command.output() { + Ok(output) => { + if !output.status.success() { + return Err(CompilerError::FailedBuildingFixture.into()); + } + } + Err(_) => { + return Err(CompilerError::FailedBuildingFixture.into()); + } + } + drop(timing_guard); + + let data = fs::read_to_string(&intermediate_file) + .map_err(|io_err| CompilerError::ReadInputFailed(io_err, intermediate_file.clone()))?; + + // TODO figure out how to replace multiple substrings in a single pass. One neat trick + // might be to allocate enough space in our starting buffer to write the new code by + // having a really large replace block. For example if we are replacing a string + // `"REPLACE_ME" + "abc" * 2000` we would have over 6k bytes to write out the new code. We + // will have to do some extra book keeping to make sure the buffer space is big enough + // for the replacement code. + let span = info_span!("munge fixture javascript"); + let timing_guard = span.enter(); + let final_script = (|| { + let mut final_script = data + .replace("'REPLACE_ME_WITH_JSON_STRINGIFY'", "JSON.stringify(x)") + .replace( + "$elm$json$Json$Decode$fail('REPLACE_ME_WITH_BYTES_DECODER');", + r#" _Json_decodePrim(function(value) { + return (typeof value === 'object' && value instanceof DataView) + ? $elm$core$Result$Ok(value) + : _Json_expecting('a DataView', value); + });"#, + ) + .replace(";}(this));", ";}(globalThis));"); + + if sqlite_path.is_some() { + final_script = final_script + .replace( + "var $author$project$Astrid$Query$execute = function (query) {\n\treturn $author$project$Astrid$Query$dummyExecute;\n};", + include_str!("fixtures/sql-client-integration.js"), + ) + .replace( + "var $author$project$Astrid$Query$fetch = F3(\n\tfunction (sql, parameters, decoder) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});", + "var $author$project$Astrid$Query$fetch = _Query_fetchAll;", + ) + .replace( + "var $author$project$Astrid$Query$fetchOne = F3(\n\tfunction (sql, parameters, decoder) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});", + "var $author$project$Astrid$Query$fetchOne = _Query_fetchOne;", + ) + .replace( + "var $author$project$Astrid$Query$map5 = F6(\n\tfunction (f, a, b, c, d, e) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});", + r#"var $author$project$Astrid$Query$map5 = _Query_map5;"#, + ) + .replace( + "var $author$project$Astrid$Query$map4 = F5(\n\tfunction (f, a, b, c, d) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});", + r#"var $author$project$Astrid$Query$map4 = _Query_map4;"#, + ) + .replace( + "var $author$project$Astrid$Query$map3 = F4(\n\tfunction (f, a, b, c) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});", + r#"var $author$project$Astrid$Query$map3 = _Query_map3;"#, + ) + .replace( + "var $author$project$Astrid$Query$map2 = F3(\n\tfunction (f, a, b) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});", + r#"var $author$project$Astrid$Query$map2 = _Query_map2;"#, + ) + .replace( + "var $author$project$Astrid$Query$map = F2(\n\tfunction (f, a) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});", + r#"var $author$project$Astrid$Query$map = _Query_map1;"#, + ) + .replace( + "var $author$project$Astrid$Query$andThen = F2(\n\tfunction (f, q) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});", + r#"var $author$project$Astrid$Query$andThen = _Query_andThen;"#, + ); + + final_script.push_str("\n\n"); + } + + final_script.push_str("\n\n"); + // I think that when I set this script to be the main module, I am skipping the + // deno/runtime/js/99_main.js script that sets up a bunch of global variables. If I + // manually add the timer related code below then setTimeout works again. + // NB. there are 706 lines of setup code that add a bunch of apis to the global window + // scope. Figure out if I need to include all of them. For example, starmelon does not need + // to perform http calls right now, but I eventually want to. + final_script.push_str("const { setTimeout } = globalThis.__bootstrap.timers;\n"); + final_script.push_str( + "Deno.core.setMacrotaskCallback(globalThis.__bootstrap.timers.handleTimerMacrotask);\n", + ); + final_script.push_str("globalThis.setTimeout = setTimeout;\n"); + + final_script.push_str(&format!( + "var worker = Elm.{}.init({{flags: {{ stagename: \"Atago\"}} }});\n", + &gen_module_name + )); + // add a shortcut for invoking the function so I don't have to traverse so many object + // lookups using the rust v8 API. + final_script.push_str( + "globalThis.runOnInput = function(route) { worker.ports.onRequest.send(route) };\n", + ); + + final_script.push_str( + r#" + worker.ports.onStringOutput.subscribe(function(result) { + Deno.core.opSync('op_starmelon_string_output', result); + }); + // Elm will send a DataView + if (worker.ports.onBytesOutput) { + worker.ports.onBytesOutput.subscribe(function(result){ + if (result.$ === "Ok") { + const ui8 = new Uint8Array(result.a.buffer); + output.a = ui8; + } + Deno.core.opSync('op_starmelon_bytes_output', result) + }); + }"#, + ); + + final_script + })(); + drop(timing_guard); + + let desired_route = entrypoint.0.module.clone().to_string(); + let foo = move |mut scope: rusty_v8::HandleScope| -> Result<(), InterpreterError> { + let scope = &mut scope; + let ctx = scope.get_current_context(); + let global = ctx.global(scope); + + let entrypoint = { + let x = + v8::String::new(scope, "runOnInput").ok_or(InterpreterError::AllocationFailed)?; + v8::Local::new(scope, x).into() + }; + let v8_value = global + .get(scope, entrypoint) + .ok_or(InterpreterError::ReferenceError)?; + + // step 12 invoke the function + let function = v8::Local::::try_from(v8_value)?; + + let this = v8::undefined(scope).into(); + + let span = info_span!("dispatch v8 call"); + let timing_guard = span.enter(); + let arg1 = { + let x = + v8::String::new(scope, &desired_route).ok_or(InterpreterError::AllocationFailed)?; + v8::Local::new(scope, x).into() + }; + + function.call(scope, this, &[arg1]); + drop(timing_guard); + + Ok(()) + }; + + let mut final_file = generator_dir.join("bin").join(&gen_module_name); + final_file.set_extension("js"); + let span = info_span!("file writes"); + let timing_guard = span.enter(); + std::fs::write(&final_file, final_script) + .map_err(|io_err| CompilerError::WriteOutputFailed(io_err, final_file.clone()))?; + drop(timing_guard); + + // Create a tokio runtime before registering ops so we can block on futures inside sync ops + let span = info_span!("create tokio runtime"); + let timing_guard = span.enter(); + let sys = tokio::runtime::Builder::new_current_thread() + // The default number of additional threads for running blocking FnOnce is 512. + .max_blocking_threads(1) + .enable_all() + .build() + .unwrap(); + drop(timing_guard); + + // step 10 create a v8 isolate. We need to register a different callback depending on + // the output type (string or bytes) + + let span = info_span!("create v8 isolate"); + let timing_guard = span.enter(); + let (mut worker, main_module) = runtime::setup_worker(&final_file.to_string_lossy()) + .map_err(|err| InterpreterError::EventLoop(err))?; + drop(timing_guard); + + let span = info_span!("register private api"); + let timing_guard = span.enter(); + let mailbox: Arc, ScriptError>>>> = Arc::new(RefCell::new(None)); + + let mailbox_clone = Arc::clone(&mailbox); + worker.js_runtime.register_op( + "op_starmelon_bytes_output", + deno_core::op_sync( + move |_state, msg: ElmResult, _: ()| { + if let Ok(mut mailbox) = mailbox_clone.try_borrow_mut() { + match msg { + ElmResult::Ok { a: buffer } => { + let slice: &[u8] = &buffer; + mailbox.replace(Ok(slice.to_owned())); + } + ElmResult::Err { a: err } => { + mailbox.replace(Err(err)); + } + } + } + Ok(()) + }, + ), + ); + + let mailbox_clone = Arc::clone(&mailbox); + worker.js_runtime.register_op( + "op_starmelon_string_output", + deno_core::op_sync(move |_state, msg: ElmResult, _: ()| { + if let Ok(mut mailbox) = mailbox_clone.try_borrow_mut() { + match msg { + ElmResult::Ok { a: s } => mailbox.replace(Ok(s.into_bytes())), + ElmResult::Err { a: err } => mailbox.replace(Err(err)), + }; + } + Ok(()) + }), + ); + + // Step 10.B setup the sqlite database feature + let sql_background_thread_handle = if let Some(database_url) = sqlite_path { + // I want to construct the connection in the initial thread so I can tell if the connection + // failed + let db_pool = sys + .block_on(async { SqlitePool::connect(&database_url.to_string_lossy()).await }) + .unwrap(); + + let (worker_mailbox, rx) = std::sync::mpsc::channel::<( + oneshot::Sender>, + Vec<(bool, String, Vec)>, + )>(); + + let sql_worker_thread = std::thread::spawn(move || { + let worker = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + loop { + if let Ok((response, queries)) = rx.recv() { + // I am not sure if I should only work on one database task at a time, or + // submit as many takes as possible. Just spawning the future onto this + // exectutor does not seem to work, even though the docs say the thread pool + // will poll the future until it completes. + let db_pool_clone = db_pool.clone(); + let span = info_span!("inside sql queries futures"); + let f = async move { + let _start = Instant::now(); + let db_pool = db_pool_clone; + let mut result: Vec> = vec![]; + let mut failure: Option = None; + for (fetch_all, sql, _args) in queries { + let mut acc = Vec::new(); + if fetch_all { + let mut stream = sqlx::query(&sql).fetch(&db_pool); + loop { + match stream.next().await { + None => break, + Some(Ok(row)) => { + match row.try_get::(0) { + Ok(s) => acc.push(s), + // TODO set an error flag before returning this one + Err(_) => break, + }; + } + Some(Err(err)) => { + eprintln!("got fetch_all sql error {:?}", err); + failure = Some(err.to_string()); + break; + } + } + } + result.push(acc); + } else { + match sqlx::query(&sql) + .fetch_one(&db_pool) + .await + .and_then(|row| row.try_get::(0)) + { + Ok(s) => result.push(vec![s]), + Err(err) => { + eprintln!("got fetchOne sql error {:?}", err); + failure = Some(err.to_string()); + } + } + } + if failure.is_some() { + break; + } + } + if let Some(msg) = failure { + response.send(ElmResult::err(msg)) + } else { + response.send(ElmResult::ok(result)) + } + }; + + // I found it interesting that the runtime of the future from the viewpoint of + // tracing was around 230us for a trivial select 2 rows query, but walltime I + // measured was around 700us. So polling the future or waiting for file IO is + // more expensive than I thought. + worker.block_on(f.instrument(span)).unwrap(); + } else { + break; + } + } + }); + let worker_mailbox_clone = worker_mailbox.clone(); + + worker.js_runtime.register_op( + "op_starmelon_batch_queries", + deno_core::op_sync( + move |_state, queries: Vec<(bool, String, Vec)>, _: ()| { + let worker_mailbox = worker_mailbox_clone.clone(); + let (sender, receiver) = + oneshot::channel::>, String>>(); + + let span = info_span!("run sql"); + let timing_guard = span.enter(); + + worker_mailbox.send((sender, queries)).unwrap(); + let elm_result = receiver.recv().unwrap(); + + drop(timing_guard); + + Ok(elm_result) + }, + ), + ); + + Some((worker_mailbox, sql_worker_thread)) + } else { + None + }; + + worker.js_runtime.sync_ops_cache(); + drop(timing_guard); + + let span = info_span!("eval javascript"); + let timing_guard = span.enter(); + sys.block_on(async move { runtime::xyz(worker, main_module, foo).await })?; + drop(timing_guard); + + // step 13 receive the callback + // If I understood which combination of run_event_loop was required to execute + // the javascript to completion then we should have something in our mailbox by + // now. Another way to do this would be with an Arc. This will panic if the + // mailbox is currently borrowed + match mailbox.replace(None) { + Some(Ok(buffer)) => match output { + None => { + io::stdout() + .write_all(&buffer) + .map_err(|io_err| CompilerError::WriteOutputFailed(io_err, "stdout".into()))?; + } + Some(filename) => { + let mut f = fs::File::create(&filename) + .map_err(|io_err| CompilerError::WriteOutputFailed(io_err, filename))?; + + f.write_all(&buffer) + .map_err(|io_err| CompilerError::WriteOutputFailed(io_err, "stdout".into()))?; + } + }, + Some(Err(problem)) => { + println!("had a problem {:?}", problem); + } + None => println!("nothing in the mailbox"), + } + + if let Some((tx, thread_handle)) = sql_background_thread_handle { + drop(tx); + thread_handle.join().unwrap(); + } - eprintln!("todo implement astrid pages Route mode"); Ok(()) } diff --git a/src/exec/fixtures/astrid_pages.rs b/src/exec/fixtures/astrid_pages.rs new file mode 100644 index 0000000..d2c8001 --- /dev/null +++ b/src/exec/fixtures/astrid_pages.rs @@ -0,0 +1,218 @@ +use crate::exec::astrid_pages::OutputType; +use elm_quote::Tokens; +use genco::tokens::quoted; +use serde::{Deserialize, Serialize}; + +// I manually made this derived deserialize implementation match the elm json encoder output. The +// elm json encoder is defined in this file below. `encodeFailure : Error -> Json.Encode.Value` +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "$")] +pub enum ScriptError { + FlagsDecodeFailed { a: String }, + QueryFailed { a: String }, + HtmlGenerationFailed { a: String }, + NotFound, +} + +pub(crate) fn generate( + source_checksum: u64, + flags_module: String, + target_modules: &[(String, OutputType)], +) -> (String, String) { + let tokens: Tokens = genco::quote! { + import Astrid.Pages exposing (Route(..)) + import Astrid.Query + import Bytes exposing (Bytes) + import ElmHtml.InternalTypes exposing (decodeElmHtml) + import ElmHtml.ToString exposing (nodeToStringWithOptions, defaultFormatOptions) + import Html exposing (Html) + import Json.Decode as D + import Json.Encode as E + import Platform + + -- START CUSTOMIZED PART + import #(flags_module) exposing (Flags) + #(for (target_module, _) in target_modules.iter() => + import #(target_module) + # + ) + + dispatch route flags = + case route of + #(for (target_module, output_type) in target_modules.iter() => + #(" ")#(quoted(target_module)) -> + #( match output_type { + OutputType::String => { + #(" ")evalRoute (onStringOutput << encodeString) flags #(target_module).route + } + OutputType::Value => { + #(" ")evalRoute (onStringOutput << encodeJson) flags #(target_module).route + } + OutputType::Bytes => { + #(" ")evalRoute (onStringOutput << encodeBytes) flags #(target_module).route + } + OutputType::Html => { + #(" ")evalRoute (onStringOutput << encodeHtml) flags #(target_module).route + } + }) + # + ) + # + + _ -> + onStringOutput (encodeFailure NotFound) + + -- END CUSTOMIZED PART + + -- MAIN + + main = + Platform.worker + { init = init + , update = update + , subscriptions = subscriptions + } + + -- PORTS + + port onRequest : (String -> msg) -> Sub msg + port onStringOutput : E.Value -> Cmd msg + port onBytesOutput : E.Value -> Cmd msg + + -- MODEL + + + + init : Flags -> (Flags, Cmd Msg) + init flags = + (flags, Cmd.none) + + -- UPDATE + + type Msg + = DispatchRoute String + + update : Msg -> Flags -> (Flags, Cmd Msg) + update msg flags = + case msg of + DispatchRoute route -> + let + cmd = + dispatch route flags + in + (flags, cmd) + + evalRoute : (output -> Cmd msg) -> flags -> Route flags model output -> Cmd msg + evalRoute reportResponse flags (Route { handler, view }) = + case Astrid.Query.execute (handler flags) of + Ok model -> + reportResponse (view flags model) + + Err err -> + onStringOutput (encodeFailure (QueryFailed (Astrid.Query.errorToString err))) + + + type Error + = FlagsDecodeFailed D.Error + | QueryFailed String + | HtmlGenerationFailed String + | NotFound + + -- SUBSCRIPTIONS + + subscriptions : model -> Sub Msg + subscriptions _ = + onRequest DispatchRoute + + -- ENCODERS + + encodeFailure : Error -> E.Value + encodeFailure err = + E.object + [ ("$", E.string "Err") + , ("a" + , case err of + FlagsDecodeFailed jsonError -> + E.object + [ ("$", E.string "FlagsDecodeFailed" ) + , ("a", E.string (D.errorToString jsonError)) + ] + + QueryFailed msg -> + E.object + [ ("$", E.string "QueryFailed" ) + , ("a", E.string msg) + ] + + HtmlGenerationFailed msg -> + E.object + [ ("$", E.string "HtmlGenerationFailed" ) + , ("a", E.string msg) + ] + + NotFound -> + E.object + [ ("$", E.string "NotFound" ) + ] + ) + ] + + encodeSuccess : E.Value -> E.Value + encodeSuccess value = + E.object + [ ("ctor", E.string "Ok") + , ("a", value) + ] + + encodeJson : E.Value -> E.Value + encodeJson v = + encodeString (E.encode 0 v) + + encodeBytes : Bytes -> E.Value + encodeBytes bytes = + bytes + |> encodeBytesHelp + |> encodeSuccess + + encodeBytesHelp : Bytes -> E.Value + encodeBytesHelp bytes = + E.string "REPLACE_ME_WITH_BYTES_ENCODER" + + encodeString : String -> E.Value + encodeString s = + encodeSuccess (E.string s) + + encodeHtml : Html msg -> E.Value + encodeHtml node = + let + options = + { defaultFormatOptions | newLines = True, indent = 2 } + in + case + D.decodeString + (decodeElmHtml ( #("\\")taggers eventHandler -> D.succeed ())) + (asJsonString node) + of + Ok elmHtml -> + elmHtml + |> nodeToStringWithOptions options + |> encodeString + + Err error -> + encodeFailure (HtmlGenerationFailed (D.errorToString error)) + + + asJsonString : Html msg -> String + asJsonString x = "REPLACE_ME_WITH_JSON_STRINGIFY" + }; + + let module_name = format!("Route{:020}", source_checksum); + + let source = format!( + "port module {} exposing (main)\n\n{}", + module_name, + tokens.to_file_string().unwrap(), + ); + + (module_name, source) +} diff --git a/src/exec/fixtures/mod.rs b/src/exec/fixtures/mod.rs index abf25f7..171856a 100644 --- a/src/exec/fixtures/mod.rs +++ b/src/exec/fixtures/mod.rs @@ -1 +1,2 @@ +pub mod astrid_pages; pub mod scripting; diff --git a/src/exec/mod.rs b/src/exec/mod.rs index b78b316..ce97eb5 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -2,9 +2,9 @@ use crate::elm; use crate::reporting::{CompilerError, Problem}; use crate::PortableHash; use std::hash::Hasher; -use std::io::{Write}; -use std::path::{PathBuf}; -use tracing::{info_span}; +use std::io::Write; +use std::path::PathBuf; +use tracing::info_span; mod astrid_pages; mod fixtures; @@ -127,9 +127,7 @@ mod runtime { use deno_runtime::worker::WorkerOptions; use deno_runtime::BootstrapOptions; use deno_web::BlobStore; - - use std::pin::Pin; use std::rc::Rc; use std::sync::Arc; diff --git a/src/exec/scripting.rs b/src/exec/scripting.rs index 88e4447..36cde16 100644 --- a/src/exec/scripting.rs +++ b/src/exec/scripting.rs @@ -1,3 +1,4 @@ +use crate::exec::{fixtures, runtime}; use crate::reporting::{CompilerError, InterpreterError, Problem, TypeError}; use deno_core::futures::StreamExt; use elm_project_utils::{setup_generator_project, ElmResult}; @@ -7,15 +8,14 @@ use sqlx::sqlite::SqlitePool; use sqlx::Row; use std::cell::RefCell; use std::convert::TryFrom; -use std::fs::{self}; +use std::fs; use std::io::{self, Read, Write}; -use std::path::{PathBuf}; +use std::path::PathBuf; use std::process::{Command, Stdio}; use std::sync::Arc; use std::time::Instant; use tokio; use tracing::{info_span, Instrument}; -use crate::exec::{fixtures, runtime}; pub(crate) fn run( debug: bool, diff --git a/src/reporting.rs b/src/reporting.rs index c62e439..151b131 100644 --- a/src/reporting.rs +++ b/src/reporting.rs @@ -82,9 +82,10 @@ impl From for Problem { ProjectSetupError::ReadFailed { filename, source } => { CompilerError::ReadInputFailed(source, filename).into() } - ProjectSetupError::FailedParseElmJson { filename: _, source } => { - CompilerError::FailedParseElmJson(source).into() - } + ProjectSetupError::FailedParseElmJson { + filename: _, + source, + } => CompilerError::FailedParseElmJson(source).into(), ProjectSetupError::InstallDependencyFailed => { SetupError::InstallDependencyFailed.into() }