Embed a 'welcome' graph to fallback on
Some checks are pending
/ verify (push) Waiting to run
/ publish (push) Waiting to run

This commit is contained in:
Juno Takano 2026-03-22 18:50:15 -03:00
commit dfddbba4ef
5 changed files with 123 additions and 35 deletions

18
Cargo.lock generated
View file

@ -259,7 +259,7 @@ dependencies = [
[[package]] [[package]]
name = "en" name = "en"
version = "0.3.1-alpha" version = "0.4.0-alpha"
dependencies = [ dependencies = [
"axum", "axum",
"serde", "serde",
@ -1209,9 +1209,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]] [[package]]
name = "ureq" name = "ureq"
version = "3.2.0" version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdc97a28575b85cfedf2a7e7d3cc64b3e11bd8ac766666318003abbacc7a21fc" checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0"
dependencies = [ dependencies = [
"base64", "base64",
"flate2", "flate2",
@ -1220,15 +1220,15 @@ dependencies = [
"rustls", "rustls",
"rustls-pki-types", "rustls-pki-types",
"ureq-proto", "ureq-proto",
"utf-8", "utf8-zero",
"webpki-roots", "webpki-roots",
] ]
[[package]] [[package]]
name = "ureq-proto" name = "ureq-proto"
version = "0.5.3" version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f" checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c"
dependencies = [ dependencies = [
"base64", "base64",
"http", "http",
@ -1237,10 +1237,10 @@ dependencies = [
] ]
[[package]] [[package]]
name = "utf-8" name = "utf8-zero"
version = "0.7.6" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
[[package]] [[package]]
name = "version_check" name = "version_check"

View file

@ -1,6 +1,6 @@
[package] [package]
name = "en" name = "en"
version = "0.3.1-alpha" version = "0.4.0-alpha"
description = "A non-linear writing instrument." description = "A non-linear writing instrument."
license = "AGPL-3.0-only" license = "AGPL-3.0-only"

View file

@ -1,4 +1,4 @@
use std::{collections::HashMap, path::PathBuf}; use std::{collections::HashMap, io, path::PathBuf};
pub use edge::Edge; pub use edge::Edge;
pub use meta::{Config, Meta}; pub use meta::{Config, Meta};
@ -43,6 +43,18 @@ pub struct Stats {
} }
impl Graph { impl Graph {
fn welcome() -> Graph {
let toml = include_str!("../static/welcome.toml");
let mut welcome_graph = match Graph::from_serial(toml, &Format::TOML) {
Ok(graph) => graph,
Err(error) => {
panic!("Welcome graph parsing must be infallible: {error:?}")
},
};
welcome_graph.modulate();
welcome_graph
}
pub fn with_message(message: &str) -> Graph { pub fn with_message(message: &str) -> Graph {
let graph = Graph::default(); let graph = Graph::default();
let mut messages = graph.meta.messages; let mut messages = graph.meta.messages;
@ -69,12 +81,23 @@ impl Graph {
/// Loads a Graph TOML file from CLI arguments or their defaults and /// Loads a Graph TOML file from CLI arguments or their defaults and
/// returns a modulated Graph. /// returns a modulated Graph.
/// ///
/// Loads a default graph with basic usage instructions if no file is found.
///
/// Returns a graph with an error message if any errors are propagated. /// Returns a graph with an error message if any errors are propagated.
pub fn load() -> Graph { pub fn load() -> Graph {
let result = Graph::load_file(None); let result = Graph::load_file(None);
match result { match result {
Ok(graph) => graph, Ok(graph) => graph,
Err(error) => Graph::malformed(Some(&error)), Err(error) => {
if error.not_found {
return Graph::welcome()
}
if let Some(message) = error.message {
Graph::malformed(Some(&message))
} else {
Graph::malformed(None)
}
},
} }
} }
@ -84,7 +107,7 @@ impl Graph {
/// ///
/// # Errors /// # Errors
/// Propagates errors from `Graph::read_file`. /// Propagates errors from `Graph::read_file`.
pub fn load_file(path: Option<&str>) -> Result<Graph, String> { pub fn load_file(path: Option<&str>) -> Result<Graph, LoadError> {
let mut graph = Graph::from_file(path)?; let mut graph = Graph::from_file(path)?;
graph.modulate(); graph.modulate();
Ok(graph) Ok(graph)
@ -95,21 +118,24 @@ impl Graph {
/// # Errors /// # Errors
/// Returns Err if it can't read the contents of `in_path`. /// Returns Err if it can't read the contents of `in_path`.
/// Propagates errors from `Graph::from_serial`. /// Propagates errors from `Graph::from_serial`.
pub fn from_file(in_path: Option<&str>) -> Result<Graph, String> { pub fn from_file(in_path: Option<&str>) -> Result<Graph, LoadError> {
let cli_path = Arguments::default().parse().graph_path; let cli_path = Arguments::default().parse().graph_path;
let path = in_path.map_or(cli_path, PathBuf::from); let path = in_path.map_or(cli_path, PathBuf::from);
let toml_source = match std::fs::read_to_string(&path) { let toml_source = match std::fs::read_to_string(&path) {
Ok(s) => s, Ok(s) => s,
Err(e) => { Err(error) => {
log!( log!(
ERROR, ERROR,
"Error reading path {}: {e}", "Error reading path {}: {error}",
path.as_path().display(), path.as_path().display(),
); );
return Err(format!( return Err(LoadError::from_io_with_message(
"Failed reading file at {}", &format!(
path.as_path().display(), "Failed reading file at {}",
path.as_path().display(),
),
error,
)); ));
}, },
}; };
@ -489,12 +515,60 @@ impl Graph {
} }
} }
#[derive(Debug)]
pub enum Format { pub enum Format {
TOML, TOML,
JSON, JSON,
Unsupported, Unsupported,
} }
#[derive(Debug)]
pub struct LoadError {
pub message: Option<String>,
pub not_found: bool,
pub io_error: Option<io::Error>,
pub serial_error: Option<SerialError>,
}
impl LoadError {
fn from_io_with_message(message: &str, io_error: io::Error) -> LoadError {
LoadError {
message: Some(String::from(message)),
not_found: io_error.kind() == io::ErrorKind::NotFound,
io_error: Some(io_error),
serial_error: None,
}
}
}
impl From<SerialError> for LoadError {
fn from(error: SerialError) -> LoadError {
LoadError {
message: Some(error.message.clone()),
not_found: false,
serial_error: Some(error),
io_error: None,
}
}
}
impl From<io::Error> for LoadError {
fn from(error: io::Error) -> LoadError {
LoadError {
message: Some(error.to_string()),
not_found: error.kind() == io::ErrorKind::NotFound,
io_error: Some(error),
serial_error: None,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct SerialError {
pub cause: SerialErrorCause,
pub message: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub enum SerialErrorCause { pub enum SerialErrorCause {
UnsupportedFormat, UnsupportedFormat,
@ -511,12 +585,6 @@ impl std::fmt::Display for SerialErrorCause {
} }
} }
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct SerialError {
pub cause: SerialErrorCause,
pub message: String,
}
impl From<SerialError> for String { impl From<SerialError> for String {
fn from(error: SerialError) -> String { fn from(error: SerialError) -> String {
format!("{}: {}", error.cause, error.message) format!("{}: {}", error.cause, error.message)
@ -1314,12 +1382,11 @@ mod serial_tests {
use crate::dev::test::{Directories, Error}; use crate::dev::test::{Directories, Error};
#[test] #[test]
fn bad_graph_path() -> Result<(), Error> { fn no_graph_fallback() -> Result<(), Error> {
let _dirs = Directories::setup("bad_graph_path")?; let _dirs = Directories::setup("bad_graph_path")?;
let graph = Graph::load(); let graph = Graph::load();
let message = graph.meta.messages.first().unwrap(); assert_eq!(graph.nodes["GettingStarted"].title, "Getting Started");
assert!(message.contains("Failed reading file at"));
Ok(()) Ok(())
} }

View file

@ -137,15 +137,14 @@ fn load_templates() -> Result<tera::Tera, tera::Error> {
let root = PathBuf::from("templates"); let root = PathBuf::from("templates");
let default_names: Vec<&str> = DEFAULTS.iter().map(|(n, _)| *n).collect(); let default_names: Vec<&str> = DEFAULTS.iter().map(|(n, _)| *n).collect();
log!(
DEBUG,
"Reading templates from {}, canonical form {:?}",
root.display(),
root.canonicalize()
);
match fs::read_dir(&root) { match fs::read_dir(&root) {
Ok(dir) => { Ok(dir) => {
log!(
DEBUG,
"Reading templates from root directory '{}', canonically {:?}",
root.display(),
root.canonicalize()
);
for file_opt in dir { for file_opt in dir {
let file = file_opt?; let file = file_opt?;
let path = file.path(); let path = file.path();
@ -168,6 +167,11 @@ fn load_templates() -> Result<tera::Tera, tera::Error> {
} }
}, },
Err(error) => { Err(error) => {
log!(
VERBOSE,
"A 'templates' directory was not found or is not accessible: \
only built-in templates will be available"
);
if error.kind() != ErrorKind::NotFound { if error.kind() != ErrorKind::NotFound {
return Err(tera::Error::msg(error.to_string())) return Err(tera::Error::msg(error.to_string()))
} }

17
static/welcome.toml Normal file
View file

@ -0,0 +1,17 @@
[nodes.GettingStarted]
title = "Getting Started"
text = """
## Welcome to en!
#
If you are seeing this, it's working!
Now that you know how to run it, tell en how to find your graph file by adding a `--graph` option:
`
en --graph my_graph.toml
`
Alternatively, you can also add a `static` directory next to the en binary with a `graph.toml` file in it.
To learn how to write your first graph and everything else about en, check out the |documentation|https://en.jutty.dev|.
"""