feat: PoC for generating webpage with database

This commit is contained in:
YetAnotherMinion 2022-01-12 04:56:15 +00:00 committed by nobody
commit 028cc115a0
Signed by: GrocerPublishAgent
GPG key ID: D460CD54A9E3AB86
8 changed files with 780 additions and 22 deletions

View file

@ -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"

View file

@ -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);

View file

@ -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<PathBuf>,
_elm_project_dir: PathBuf,
_source_checksum: u64,
_entrypoint: elmi::Global,
_output: Option<PathBuf>,
debug: bool,
verbosity: u64,
sqlite_path: Option<PathBuf>,
elm_project_dir: PathBuf,
source_checksum: u64,
entrypoint: elmi::Global,
output: Option<PathBuf>,
) -> 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::<v8::Function>::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<RefCell<Option<Result<Vec<u8>, 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<deno_core::ZeroCopyBuf, ScriptError>, _: ()| {
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<String, ScriptError>, _: ()| {
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<ElmResult<_, _>>,
Vec<(bool, String, Vec<String>)>,
)>();
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<String>> = vec![];
let mut failure: Option<String> = 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::<String, _>(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::<String, _>(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<String>)>, _: ()| {
let worker_mailbox = worker_mailbox_clone.clone();
let (sender, receiver) =
oneshot::channel::<ElmResult<Vec<Vec<String>>, 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(())
}

View file

@ -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)
#<push>
)
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
}
})
#<push>
)
#<line>
_ ->
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)
}

View file

@ -1 +1,2 @@
pub mod astrid_pages;
pub mod scripting;

View file

@ -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;
@ -128,8 +128,6 @@ mod runtime {
use deno_runtime::BootstrapOptions;
use deno_web::BlobStore;
use std::pin::Pin;
use std::rc::Rc;
use std::sync::Arc;

View file

@ -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,

View file

@ -82,9 +82,10 @@ impl From<elm_project_utils::ProjectSetupError> 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()
}