From e70a8cc230c656c2a4f107aee1dcc4d2b7745c1e Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Tue, 6 Feb 2024 10:02:30 +1300 Subject: [PATCH 01/38] Split `cli` and `cli_model` crates from `rt_model_native`. --- CHANGELOG.md | 9 ++++ Cargo.toml | 15 +++++-- crate/cli/Cargo.toml | 42 +++++++++++++++++++ crate/cli/src/lib.rs | 5 +++ crate/{rt_model_native => cli}/src/output.rs | 0 .../src/output/cli_colorize.rs | 0 .../src/output/cli_colorize_opt.rs | 0 .../src/output/cli_colorize_parse_error.rs | 0 .../src/output/cli_md_presenter.rs | 0 .../src/output/cli_output.rs | 7 +--- .../src/output/cli_output_builder.rs | 3 +- .../src/output/cli_output_target.rs | 0 .../src/output/cli_progress_format.rs | 0 .../src/output/cli_progress_format_opt.rs | 0 .../cli_progress_format_opt_parse_error.rs | 0 crate/cli_model/Cargo.toml | 19 +++++++++ crate/cli_model/src/lib.rs | 6 +++ .../output => cli_model/src}/output_format.rs | 2 +- .../src}/output_format_parse_error.rs | 0 crate/fmt/Cargo.toml | 3 -- crate/rt_model/src/lib.rs | 7 ---- crate/rt_model_core/src/output.rs | 7 +--- crate/rt_model_native/Cargo.toml | 12 +----- crate/rt_model_native/src/lib.rs | 1 - crate/rt_model_web/Cargo.toml | 3 +- examples/download/Cargo.toml | 2 +- examples/download/src/download_args.rs | 2 +- examples/download/src/main.rs | 3 +- examples/envman/Cargo.toml | 2 +- examples/envman/src/main_cli.rs | 2 +- examples/envman/src/model/cli_args.rs | 5 +-- src/lib.rs | 4 ++ workspace_tests/Cargo.toml | 2 +- workspace_tests/src/cli.rs | 1 + .../src/{rt_model => cli}/output.rs | 2 - .../output/cli_colorize_opt.rs | 2 +- .../output/cli_colorize_opt_parse_error.rs | 2 +- .../output/cli_md_presenter.rs | 5 ++- .../{rt_model => cli}/output/cli_output.rs | 6 ++- .../output/cli_output_builder.rs | 7 +++- .../output/cli_output_target.rs | 2 +- .../output/cli_progress_format_opt.rs | 2 +- .../cli_progress_format_opt_parse_error.rs | 2 +- workspace_tests/src/fmt.rs | 5 ++- workspace_tests/src/fmt/either.rs | 2 +- workspace_tests/src/fmt/presentable/bold.rs | 2 +- .../src/fmt/presentable/code_inline.rs | 2 +- .../src/fmt/presentable/heading.rs | 2 +- .../src/fmt/presentable/list_bulleted.rs | 2 +- .../fmt/presentable/list_bulleted_aligned.rs | 2 +- .../src/fmt/presentable/list_numbered.rs | 2 +- .../fmt/presentable/list_numbered_aligned.rs | 2 +- workspace_tests/src/lib.rs | 1 + workspace_tests/src/rt/cmds/diff_cmd.rs | 6 +-- workspace_tests/src/rt_model.rs | 1 - .../src/rt_model/output/output_format.rs | 41 ------------------ .../output/output_format_parse_error.rs | 27 ------------ 57 files changed, 146 insertions(+), 145 deletions(-) create mode 100644 crate/cli/Cargo.toml create mode 100644 crate/cli/src/lib.rs rename crate/{rt_model_native => cli}/src/output.rs (100%) rename crate/{rt_model_native => cli}/src/output/cli_colorize.rs (100%) rename crate/{rt_model_native => cli}/src/output/cli_colorize_opt.rs (100%) rename crate/{rt_model_native => cli}/src/output/cli_colorize_parse_error.rs (100%) rename crate/{rt_model_native => cli}/src/output/cli_md_presenter.rs (100%) rename crate/{rt_model_native => cli}/src/output/cli_output.rs (99%) rename crate/{rt_model_native => cli}/src/output/cli_output_builder.rs (99%) rename crate/{rt_model_native => cli}/src/output/cli_output_target.rs (100%) rename crate/{rt_model_native => cli}/src/output/cli_progress_format.rs (100%) rename crate/{rt_model_native => cli}/src/output/cli_progress_format_opt.rs (100%) rename crate/{rt_model_native => cli}/src/output/cli_progress_format_opt_parse_error.rs (100%) create mode 100644 crate/cli_model/Cargo.toml create mode 100644 crate/cli_model/src/lib.rs rename crate/{rt_model_core/src/output => cli_model/src}/output_format.rs (94%) rename crate/{rt_model_core/src/output => cli_model/src}/output_format_parse_error.rs (100%) create mode 100644 workspace_tests/src/cli.rs rename workspace_tests/src/{rt_model => cli}/output.rs (85%) rename workspace_tests/src/{rt_model => cli}/output/cli_colorize_opt.rs (92%) rename workspace_tests/src/{rt_model => cli}/output/cli_colorize_opt_parse_error.rs (92%) rename workspace_tests/src/{rt_model => cli}/output/cli_md_presenter.rs (99%) rename workspace_tests/src/{rt_model => cli}/output/cli_output.rs (99%) rename workspace_tests/src/{rt_model => cli}/output/cli_output_builder.rs (95%) rename workspace_tests/src/{rt_model => cli}/output/cli_output_target.rs (95%) rename workspace_tests/src/{rt_model => cli}/output/cli_progress_format_opt.rs (94%) rename workspace_tests/src/{rt_model => cli}/output/cli_progress_format_opt_parse_error.rs (92%) delete mode 100644 workspace_tests/src/rt_model/output/output_format.rs delete mode 100644 workspace_tests/src/rt_model/output/output_format_parse_error.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 8387bf998..6b64b756b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## unreleased + +* Move `Cli*` types to `peace_cli` crate under `cli::output` module. ([#182]) +* Move `OutputFormat` and `OutputFormatParseError` to `peace_cli_model` crate. ([#182]) + + +[#182]: https://github.com/azriel91/peace/issues/182 + + ## 0.0.13 (2024-02-03) * Provide more accurate feedback about interruption on CLI. ([#172], [#173]) diff --git a/Cargo.toml b/Cargo.toml index 0664fc028..e821bda10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,8 +20,10 @@ crate-type = ["cdylib", "rlib"] [dependencies] miette = { workspace = true, optional = true } peace_cfg = { workspace = true } -peace_cmd_model = { workspace = true } +peace_cli = { workspace = true, optional = true } +peace_cli_model = { workspace = true, optional = true } peace_cmd = { workspace = true } +peace_cmd_model = { workspace = true } peace_cmd_rt = { workspace = true } peace_data = { workspace = true } peace_diff = { workspace = true } @@ -33,6 +35,10 @@ peace_rt_model = { workspace = true } [features] default = [] +cli = [ + "dep:peace_cli", + "dep:peace_cli_model", +] error_reporting = [ "dep:miette", "miette?/fancy", @@ -42,8 +48,9 @@ error_reporting = [ "peace_rt/error_reporting", "peace_rt_model/error_reporting", ] -output_in_memory = ["peace_rt_model/output_in_memory"] +output_in_memory = ["peace_cli?/output_in_memory"] output_progress = [ + "peace_cli?/output_progress", "peace_cmd_rt/output_progress", "peace_cfg/output_progress", "peace_rt/output_progress", @@ -74,8 +81,10 @@ license = "MIT OR Apache-2.0" peace = { path = ".", version = "0.0.13", default-features = false } peace_cfg = { path = "crate/cfg", version = "0.0.13" } -peace_cmd_model = { path = "crate/cmd_model", version = "0.0.13" } +peace_cli = { path = "crate/cli", version = "0.0.13" } +peace_cli_model = { path = "crate/cli_model", version = "0.0.13" } peace_cmd = { path = "crate/cmd", version = "0.0.13" } +peace_cmd_model = { path = "crate/cmd_model", version = "0.0.13" } peace_cmd_rt = { path = "crate/cmd_rt", version = "0.0.13" } peace_code_gen = { path = "crate/code_gen", version = "0.0.13" } peace_core = { path = "crate/core", version = "0.0.13" } diff --git a/crate/cli/Cargo.toml b/crate/cli/Cargo.toml new file mode 100644 index 000000000..afb347083 --- /dev/null +++ b/crate/cli/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "peace_cli" +description = "Command line interface for the peace automation framework." +documentation = "https://docs.rs/peace_cli/" +version.workspace = true +authors.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true +readme.workspace = true +categories.workspace = true +keywords.workspace = true +license.workspace = true + +[lib] +doctest = true +test = false + +[dependencies] +cfg-if = { workspace = true } +console = { workspace = true } +futures = { workspace = true } +peace_cli_model = { workspace = true } +peace_core = { workspace = true } +peace_fmt = { workspace = true } +peace_rt_model_core = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +serde_yaml = { workspace = true } +tokio = { workspace = true, features = ["fs", "io-std"] } + +[target.'cfg(unix)'.dependencies] +libc = { workspace = true } +raw_tty = { workspace = true } + +[features] +default = [] +output_in_memory = ["peace_rt_model_core/output_in_memory"] +output_progress = [ + "peace_core/output_progress", + "peace_rt_model_core/output_progress", +] diff --git a/crate/cli/src/lib.rs b/crate/cli/src/lib.rs new file mode 100644 index 000000000..75bd028e0 --- /dev/null +++ b/crate/cli/src/lib.rs @@ -0,0 +1,5 @@ +//! Command line interface for the peace automation framework. +//! +//! This is enabled though the `"cli"` feature on the `peace` crate. + +pub mod output; diff --git a/crate/rt_model_native/src/output.rs b/crate/cli/src/output.rs similarity index 100% rename from crate/rt_model_native/src/output.rs rename to crate/cli/src/output.rs diff --git a/crate/rt_model_native/src/output/cli_colorize.rs b/crate/cli/src/output/cli_colorize.rs similarity index 100% rename from crate/rt_model_native/src/output/cli_colorize.rs rename to crate/cli/src/output/cli_colorize.rs diff --git a/crate/rt_model_native/src/output/cli_colorize_opt.rs b/crate/cli/src/output/cli_colorize_opt.rs similarity index 100% rename from crate/rt_model_native/src/output/cli_colorize_opt.rs rename to crate/cli/src/output/cli_colorize_opt.rs diff --git a/crate/rt_model_native/src/output/cli_colorize_parse_error.rs b/crate/cli/src/output/cli_colorize_parse_error.rs similarity index 100% rename from crate/rt_model_native/src/output/cli_colorize_parse_error.rs rename to crate/cli/src/output/cli_colorize_parse_error.rs diff --git a/crate/rt_model_native/src/output/cli_md_presenter.rs b/crate/cli/src/output/cli_md_presenter.rs similarity index 100% rename from crate/rt_model_native/src/output/cli_md_presenter.rs rename to crate/cli/src/output/cli_md_presenter.rs diff --git a/crate/rt_model_native/src/output/cli_output.rs b/crate/cli/src/output/cli_output.rs similarity index 99% rename from crate/rt_model_native/src/output/cli_output.rs rename to crate/cli/src/output/cli_output.rs index ad3135ac4..9aeca3491 100644 --- a/crate/rt_model_native/src/output/cli_output.rs +++ b/crate/cli/src/output/cli_output.rs @@ -1,11 +1,8 @@ use std::fmt::{self, Debug}; +use peace_cli_model::OutputFormat; use peace_fmt::Presentable; -use peace_rt_model_core::{ - async_trait, - output::{OutputFormat, OutputWrite}, - Error, NativeError, -}; +use peace_rt_model_core::{async_trait, output::OutputWrite, Error, NativeError}; use serde::Serialize; use tokio::io::{AsyncWrite, AsyncWriteExt, Stdout}; diff --git a/crate/rt_model_native/src/output/cli_output_builder.rs b/crate/cli/src/output/cli_output_builder.rs similarity index 99% rename from crate/rt_model_native/src/output/cli_output_builder.rs rename to crate/cli/src/output/cli_output_builder.rs index ec365986f..e753817fc 100644 --- a/crate/rt_model_native/src/output/cli_output_builder.rs +++ b/crate/cli/src/output/cli_output_builder.rs @@ -1,5 +1,6 @@ -use peace_rt_model_core::output::OutputFormat; use std::io::IsTerminal; + +use peace_cli_model::OutputFormat; use tokio::io::{AsyncWrite, Stdout}; use crate::output::{CliColorize, CliColorizeOpt, CliOutput}; diff --git a/crate/rt_model_native/src/output/cli_output_target.rs b/crate/cli/src/output/cli_output_target.rs similarity index 100% rename from crate/rt_model_native/src/output/cli_output_target.rs rename to crate/cli/src/output/cli_output_target.rs diff --git a/crate/rt_model_native/src/output/cli_progress_format.rs b/crate/cli/src/output/cli_progress_format.rs similarity index 100% rename from crate/rt_model_native/src/output/cli_progress_format.rs rename to crate/cli/src/output/cli_progress_format.rs diff --git a/crate/rt_model_native/src/output/cli_progress_format_opt.rs b/crate/cli/src/output/cli_progress_format_opt.rs similarity index 100% rename from crate/rt_model_native/src/output/cli_progress_format_opt.rs rename to crate/cli/src/output/cli_progress_format_opt.rs diff --git a/crate/rt_model_native/src/output/cli_progress_format_opt_parse_error.rs b/crate/cli/src/output/cli_progress_format_opt_parse_error.rs similarity index 100% rename from crate/rt_model_native/src/output/cli_progress_format_opt_parse_error.rs rename to crate/cli/src/output/cli_progress_format_opt_parse_error.rs diff --git a/crate/cli_model/Cargo.toml b/crate/cli_model/Cargo.toml new file mode 100644 index 000000000..d8cc06380 --- /dev/null +++ b/crate/cli_model/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "peace_cli_model" +description = "Command line interface data types for the peace automation framework." +documentation = "https://docs.rs/peace_cli/" +version.workspace = true +authors.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true +readme.workspace = true +categories.workspace = true +keywords.workspace = true +license.workspace = true + +[lib] +doctest = true +test = false + +[dependencies] diff --git a/crate/cli_model/src/lib.rs b/crate/cli_model/src/lib.rs new file mode 100644 index 000000000..c792f3592 --- /dev/null +++ b/crate/cli_model/src/lib.rs @@ -0,0 +1,6 @@ +//! Command line interface data types for the peace automation framework. + +pub use self::{output_format::OutputFormat, output_format_parse_error::OutputFormatParseError}; + +mod output_format; +mod output_format_parse_error; diff --git a/crate/rt_model_core/src/output/output_format.rs b/crate/cli_model/src/output_format.rs similarity index 94% rename from crate/rt_model_core/src/output/output_format.rs rename to crate/cli_model/src/output_format.rs index c4bca0bb8..47870d9fb 100644 --- a/crate/rt_model_core/src/output/output_format.rs +++ b/crate/cli_model/src/output_format.rs @@ -1,6 +1,6 @@ use std::str::FromStr; -use crate::output::OutputFormatParseError; +use crate::OutputFormatParseError; /// How to format command output -- human readable or machine parsable. #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/crate/rt_model_core/src/output/output_format_parse_error.rs b/crate/cli_model/src/output_format_parse_error.rs similarity index 100% rename from crate/rt_model_core/src/output/output_format_parse_error.rs rename to crate/cli_model/src/output_format_parse_error.rs diff --git a/crate/fmt/Cargo.toml b/crate/fmt/Cargo.toml index e6c8e0bcd..a6167193d 100644 --- a/crate/fmt/Cargo.toml +++ b/crate/fmt/Cargo.toml @@ -18,7 +18,4 @@ test = false [dependencies] async-trait = { workspace = true } -serde = { workspace = true } - -[dev-dependencies] serde = { workspace = true, features = ["derive"] } diff --git a/crate/rt_model/src/lib.rs b/crate/rt_model/src/lib.rs index feb4742e6..e01f71382 100644 --- a/crate/rt_model/src/lib.rs +++ b/crate/rt_model/src/lib.rs @@ -7,13 +7,6 @@ pub use peace_data::fn_graph::{self, FnRef}; pub use peace_rt_model_core::*; -pub mod output { - pub use peace_rt_model_core::output::*; - - #[cfg(not(target_arch = "wasm32"))] - pub use peace_rt_model_native::output::*; -} - #[cfg(not(target_arch = "wasm32"))] pub use peace_rt_model_native::*; diff --git a/crate/rt_model_core/src/output.rs b/crate/rt_model_core/src/output.rs index 185e7a93a..66af135e6 100644 --- a/crate/rt_model_core/src/output.rs +++ b/crate/rt_model_core/src/output.rs @@ -1,8 +1,3 @@ -pub use self::{ - output_format::OutputFormat, output_format_parse_error::OutputFormatParseError, - output_write::OutputWrite, -}; +pub use self::output_write::OutputWrite; -mod output_format; -mod output_format_parse_error; mod output_write; diff --git a/crate/rt_model_native/Cargo.toml b/crate/rt_model_native/Cargo.toml index 2036d7aa6..41eaeabe0 100644 --- a/crate/rt_model_native/Cargo.toml +++ b/crate/rt_model_native/Cargo.toml @@ -17,28 +17,18 @@ doctest = true test = false [dependencies] -cfg-if = { workspace = true } -console = { workspace = true } futures = { workspace = true } -miette = { workspace = true, optional = true } peace_core = { workspace = true } -peace_fmt = { workspace = true } peace_resources = { workspace = true } peace_rt_model_core = { workspace = true } serde = { workspace = true } -serde_json = { workspace = true } serde_yaml = { workspace = true } -thiserror = { workspace = true } tokio = { workspace = true, features = ["fs", "io-std"] } tokio-util = { workspace = true, features = ["io", "io-util"] } -[target.'cfg(unix)'.dependencies] -libc = { workspace = true } -raw_tty = { workspace = true } - [features] default = [] -error_reporting = ["dep:miette", "peace_rt_model_core/error_reporting"] +error_reporting = ["peace_rt_model_core/error_reporting"] output_in_memory = ["peace_rt_model_core/output_in_memory"] output_progress = [ "peace_core/output_progress", diff --git a/crate/rt_model_native/src/lib.rs b/crate/rt_model_native/src/lib.rs index efa5a4e35..69eb3595a 100644 --- a/crate/rt_model_native/src/lib.rs +++ b/crate/rt_model_native/src/lib.rs @@ -13,7 +13,6 @@ pub use crate::{ workspace_initializer::WorkspaceInitializer, workspace_spec::WorkspaceSpec, }; -pub mod output; pub mod workspace; mod storage; diff --git a/crate/rt_model_web/Cargo.toml b/crate/rt_model_web/Cargo.toml index 0ebf62a90..5029f8198 100644 --- a/crate/rt_model_web/Cargo.toml +++ b/crate/rt_model_web/Cargo.toml @@ -18,7 +18,6 @@ test = false [dependencies] base64 = { workspace = true } -miette = { workspace = true, optional = true } peace_core = { workspace = true } peace_resources = { workspace = true } peace_rt_model_core = { workspace = true } @@ -32,5 +31,5 @@ web-sys = { workspace = true, features = ["Storage", "Window"] } [features] default = [] -error_reporting = ["dep:miette", "peace_rt_model_core/error_reporting"] +error_reporting = ["peace_rt_model_core/error_reporting"] output_progress = [] diff --git a/examples/download/Cargo.toml b/examples/download/Cargo.toml index aa89eeb2d..093d5463b 100644 --- a/examples/download/Cargo.toml +++ b/examples/download/Cargo.toml @@ -18,7 +18,7 @@ test = false crate-type = ["cdylib", "rlib"] [dependencies] -peace = { path = "../..", default-features = false } +peace = { path = "../..", default-features = false, features = ["cli"] } peace_items = { path = "../../items", features = ["file_download"] } thiserror = "1.0.56" url = { version = "2.5.0", features = ["serde"] } diff --git a/examples/download/src/download_args.rs b/examples/download/src/download_args.rs index a80ac47b5..ce11a8746 100644 --- a/examples/download/src/download_args.rs +++ b/examples/download/src/download_args.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use clap::{Parser, Subcommand, ValueHint}; -use peace::rt_model::output::OutputFormat; +use peace::cli_model::OutputFormat; use url::Url; #[derive(Parser)] diff --git a/examples/download/src/main.rs b/examples/download/src/main.rs index 27addf161..4bc010746 100644 --- a/examples/download/src/main.rs +++ b/examples/download/src/main.rs @@ -1,7 +1,8 @@ use clap::Parser; use peace::{ cfg::{flow_id, profile}, - rt_model::{output::CliOutput, WorkspaceSpec}, + cli::output::CliOutput, + rt_model::WorkspaceSpec, }; use peace_items::file_download::FileDownloadParams; diff --git a/examples/envman/Cargo.toml b/examples/envman/Cargo.toml index 1e6b5f392..073d44814 100644 --- a/examples/envman/Cargo.toml +++ b/examples/envman/Cargo.toml @@ -28,7 +28,7 @@ chrono = { version = "0.4.33", default-features = false, features = ["clock", "s derivative = { version = "2.2.0", optional = true } futures = { version = "0.3.30", optional = true } md5-rs = { version = "0.1.5", optional = true } # WASM compatible, and reads bytes as stream -peace = { path = "../..", default-features = false } +peace = { path = "../..", default-features = false, features = ["cli"] } peace_items = { path = "../../items", features = ["file_download", "tar_x"] } semver = { version = "1.0.21", optional = true } serde = { version = "1.0.196", features = ["derive"] } diff --git a/examples/envman/src/main_cli.rs b/examples/envman/src/main_cli.rs index dcaa37203..070967f8e 100644 --- a/examples/envman/src/main_cli.rs +++ b/examples/envman/src/main_cli.rs @@ -11,7 +11,7 @@ use envman::{ EnvDiffSelection, EnvManError, ProfileSwitch, }, }; -use peace::rt_model::output::CliOutput; +use peace::cli::output::CliOutput; use tokio::io::Stdout; #[cfg(feature = "web_server")] diff --git a/examples/envman/src/model/cli_args.rs b/examples/envman/src/model/cli_args.rs index f6757b4a6..e9f1b82ee 100644 --- a/examples/envman/src/model/cli_args.rs +++ b/examples/envman/src/model/cli_args.rs @@ -2,10 +2,7 @@ use std::net::IpAddr; use clap::{Parser, Subcommand, ValueHint}; -use peace::{ - cfg::Profile, - rt_model::output::{CliColorizeOpt, OutputFormat}, -}; +use peace::{cfg::Profile, cli::output::CliColorizeOpt, cli_model::OutputFormat}; use semver::Version; use url::Url; diff --git a/src/lib.rs b/src/lib.rs index 917b278df..56d9e2d26 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,10 @@ pub use miette; pub use peace_cfg as cfg; +#[cfg(feature = "cli")] +pub use peace_cli as cli; +#[cfg(feature = "cli")] +pub use peace_cli_model as cli_model; pub use peace_cmd as cmd; pub use peace_cmd_model as cmd_model; pub use peace_cmd_rt as cmd_rt; diff --git a/workspace_tests/Cargo.toml b/workspace_tests/Cargo.toml index 4f746a650..13b14bb53 100644 --- a/workspace_tests/Cargo.toml +++ b/workspace_tests/Cargo.toml @@ -23,7 +23,7 @@ console = { workspace = true } diff-struct = { workspace = true } derivative = { workspace = true } futures = { workspace = true } -peace = { workspace = true, default-features = false } +peace = { workspace = true, default-features = false, features = ["cli"] } # `ItemWrapper` always needs the `blank` item spec to be present. peace_items = { workspace = true, features = ["blank"] } pretty_assertions = { workspace = true } diff --git a/workspace_tests/src/cli.rs b/workspace_tests/src/cli.rs new file mode 100644 index 000000000..32de1d17f --- /dev/null +++ b/workspace_tests/src/cli.rs @@ -0,0 +1 @@ +mod output; diff --git a/workspace_tests/src/rt_model/output.rs b/workspace_tests/src/cli/output.rs similarity index 85% rename from workspace_tests/src/rt_model/output.rs rename to workspace_tests/src/cli/output.rs index 9a75f15ec..e7bca3cf6 100644 --- a/workspace_tests/src/rt_model/output.rs +++ b/workspace_tests/src/cli/output.rs @@ -4,8 +4,6 @@ mod cli_md_presenter; mod cli_output; mod cli_output_builder; mod cli_output_target; -mod output_format; -mod output_format_parse_error; cfg_if::cfg_if! { if #[cfg(feature = "output_progress")] { diff --git a/workspace_tests/src/rt_model/output/cli_colorize_opt.rs b/workspace_tests/src/cli/output/cli_colorize_opt.rs similarity index 92% rename from workspace_tests/src/rt_model/output/cli_colorize_opt.rs rename to workspace_tests/src/cli/output/cli_colorize_opt.rs index 325f60e34..726cb337a 100644 --- a/workspace_tests/src/rt_model/output/cli_colorize_opt.rs +++ b/workspace_tests/src/cli/output/cli_colorize_opt.rs @@ -1,6 +1,6 @@ use std::str::FromStr; -use peace::rt_model::output::{CliColorizeOpt, CliColorizeOptParseError}; +use peace::cli::output::{CliColorizeOpt, CliColorizeOptParseError}; #[test] fn from_str_returns_ok_for_auto() { diff --git a/workspace_tests/src/rt_model/output/cli_colorize_opt_parse_error.rs b/workspace_tests/src/cli/output/cli_colorize_opt_parse_error.rs similarity index 92% rename from workspace_tests/src/rt_model/output/cli_colorize_opt_parse_error.rs rename to workspace_tests/src/cli/output/cli_colorize_opt_parse_error.rs index 47426f6e6..a44191e66 100644 --- a/workspace_tests/src/rt_model/output/cli_colorize_opt_parse_error.rs +++ b/workspace_tests/src/cli/output/cli_colorize_opt_parse_error.rs @@ -1,4 +1,4 @@ -use peace::rt_model::output::CliColorizeOptParseError; +use peace::cli::output::CliColorizeOptParseError; #[test] fn display_includes_auto_always_never() { diff --git a/workspace_tests/src/rt_model/output/cli_md_presenter.rs b/workspace_tests/src/cli/output/cli_md_presenter.rs similarity index 99% rename from workspace_tests/src/rt_model/output/cli_md_presenter.rs rename to workspace_tests/src/cli/output/cli_md_presenter.rs index 6742d4551..18517614b 100644 --- a/workspace_tests/src/rt_model/output/cli_md_presenter.rs +++ b/workspace_tests/src/cli/output/cli_md_presenter.rs @@ -1,13 +1,14 @@ use futures::stream::{self, StreamExt, TryStreamExt}; use peace::{ + cli::output::{CliMdPresenter, CliOutput, CliOutputBuilder}, + cli_model::OutputFormat, fmt::{ presentable::{Bold, CodeInline, HeadingLevel}, Presenter, }, - rt_model::output::{CliMdPresenter, CliOutput, CliOutputBuilder, OutputFormat}, }; -use peace::rt_model::output::CliColorizeOpt; +use peace::cli::output::CliColorizeOpt; #[tokio::test] async fn presents_heading_with_hashes_color_disabled() -> Result<(), Box> { diff --git a/workspace_tests/src/rt_model/output/cli_output.rs b/workspace_tests/src/cli/output/cli_output.rs similarity index 99% rename from workspace_tests/src/rt_model/output/cli_output.rs rename to workspace_tests/src/cli/output/cli_output.rs index 205fc383e..2b1ef0455 100644 --- a/workspace_tests/src/rt_model/output/cli_output.rs +++ b/workspace_tests/src/cli/output/cli_output.rs @@ -1,10 +1,12 @@ use peace::{ cfg::{item_id, State}, + cli::output::{CliColorizeOpt, CliOutput, CliOutputBuilder}, + cli_model::OutputFormat, resources::{ internal::{StateDiffsMut, StatesMut}, states::{StateDiffs, StatesCurrentStored}, }, - rt_model::output::{CliColorizeOpt, CliOutput, CliOutputBuilder, OutputFormat, OutputWrite}, + rt_model::output::OutputWrite, }; cfg_if::cfg_if! { @@ -20,9 +22,9 @@ cfg_if::cfg_if! { ProgressUpdate, ProgressUpdateAndId, }, + cli::output::{CliOutputTarget, CliProgressFormatOpt}, rt_model::{ indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget}, - output::{CliOutputTarget, CliProgressFormatOpt}, CmdProgressTracker, IndexMap, }, diff --git a/workspace_tests/src/rt_model/output/cli_output_builder.rs b/workspace_tests/src/cli/output/cli_output_builder.rs similarity index 95% rename from workspace_tests/src/rt_model/output/cli_output_builder.rs rename to workspace_tests/src/cli/output/cli_output_builder.rs index 0b1245dce..f17986856 100644 --- a/workspace_tests/src/rt_model/output/cli_output_builder.rs +++ b/workspace_tests/src/cli/output/cli_output_builder.rs @@ -1,7 +1,10 @@ -use peace::rt_model::output::{CliColorize, CliColorizeOpt, CliOutputBuilder, OutputFormat}; +use peace::{ + cli::output::{CliColorize, CliColorizeOpt, CliOutputBuilder}, + cli_model::OutputFormat, +}; #[cfg(feature = "output_progress")] -use peace::rt_model::output::{CliOutputTarget, CliProgressFormat, CliProgressFormatOpt}; +use peace::cli::output::{CliOutputTarget, CliProgressFormat, CliProgressFormatOpt}; #[tokio::test] async fn new_uses_sensible_defaults() -> Result<(), Box> { diff --git a/workspace_tests/src/rt_model/output/cli_output_target.rs b/workspace_tests/src/cli/output/cli_output_target.rs similarity index 95% rename from workspace_tests/src/rt_model/output/cli_output_target.rs rename to workspace_tests/src/cli/output/cli_output_target.rs index 4ce45701d..23e7be71b 100644 --- a/workspace_tests/src/rt_model/output/cli_output_target.rs +++ b/workspace_tests/src/cli/output/cli_output_target.rs @@ -1,4 +1,4 @@ -use peace::rt_model::output::CliOutputTarget; +use peace::cli::output::CliOutputTarget; #[test] fn clone() { diff --git a/workspace_tests/src/rt_model/output/cli_progress_format_opt.rs b/workspace_tests/src/cli/output/cli_progress_format_opt.rs similarity index 94% rename from workspace_tests/src/rt_model/output/cli_progress_format_opt.rs rename to workspace_tests/src/cli/output/cli_progress_format_opt.rs index f3008f388..4848661f9 100644 --- a/workspace_tests/src/rt_model/output/cli_progress_format_opt.rs +++ b/workspace_tests/src/cli/output/cli_progress_format_opt.rs @@ -1,6 +1,6 @@ use std::str::FromStr; -use peace::rt_model::output::{CliProgressFormatOpt, CliProgressFormatOptParseError}; +use peace::cli::output::{CliProgressFormatOpt, CliProgressFormatOptParseError}; #[test] fn from_str_returns_ok_for_auto() { diff --git a/workspace_tests/src/rt_model/output/cli_progress_format_opt_parse_error.rs b/workspace_tests/src/cli/output/cli_progress_format_opt_parse_error.rs similarity index 92% rename from workspace_tests/src/rt_model/output/cli_progress_format_opt_parse_error.rs rename to workspace_tests/src/cli/output/cli_progress_format_opt_parse_error.rs index e1baf941b..eea98ba60 100644 --- a/workspace_tests/src/rt_model/output/cli_progress_format_opt_parse_error.rs +++ b/workspace_tests/src/cli/output/cli_progress_format_opt_parse_error.rs @@ -1,4 +1,4 @@ -use peace::rt_model::output::CliProgressFormatOptParseError; +use peace::cli::output::CliProgressFormatOptParseError; #[test] fn display_includes_auto_output_pb_progress_bar() { diff --git a/workspace_tests/src/fmt.rs b/workspace_tests/src/fmt.rs index 94a69a7da..9e5aececf 100644 --- a/workspace_tests/src/fmt.rs +++ b/workspace_tests/src/fmt.rs @@ -1,4 +1,7 @@ -use peace::rt_model::output::{CliColorizeOpt, CliOutput, CliOutputBuilder, OutputFormat}; +use peace::{ + cli::output::{CliColorizeOpt, CliOutput, CliOutputBuilder}, + cli_model::OutputFormat, +}; mod either; mod presentable; diff --git a/workspace_tests/src/fmt/either.rs b/workspace_tests/src/fmt/either.rs index f051b2059..92cd19f69 100644 --- a/workspace_tests/src/fmt/either.rs +++ b/workspace_tests/src/fmt/either.rs @@ -1,9 +1,9 @@ use peace::{ + cli::output::{CliColorizeOpt, CliMdPresenter}, fmt::{ presentable::{Bold, CodeInline}, Either, Presentable, PresentableExt, }, - rt_model::output::{CliColorizeOpt, CliMdPresenter}, }; use crate::fmt::cli_output; diff --git a/workspace_tests/src/fmt/presentable/bold.rs b/workspace_tests/src/fmt/presentable/bold.rs index 529eafc8c..c00c8b133 100644 --- a/workspace_tests/src/fmt/presentable/bold.rs +++ b/workspace_tests/src/fmt/presentable/bold.rs @@ -1,6 +1,6 @@ use peace::{ + cli::output::{CliColorizeOpt, CliMdPresenter}, fmt::{presentable::Bold, Presentable}, - rt_model::output::{CliColorizeOpt, CliMdPresenter}, }; use crate::fmt::cli_output; diff --git a/workspace_tests/src/fmt/presentable/code_inline.rs b/workspace_tests/src/fmt/presentable/code_inline.rs index 3e6a5e66b..724f9215c 100644 --- a/workspace_tests/src/fmt/presentable/code_inline.rs +++ b/workspace_tests/src/fmt/presentable/code_inline.rs @@ -1,6 +1,6 @@ use peace::{ + cli::output::{CliColorizeOpt, CliMdPresenter}, fmt::{presentable::CodeInline, Presentable}, - rt_model::output::{CliColorizeOpt, CliMdPresenter}, }; use crate::fmt::cli_output; diff --git a/workspace_tests/src/fmt/presentable/heading.rs b/workspace_tests/src/fmt/presentable/heading.rs index e3e488944..ec4f9c697 100644 --- a/workspace_tests/src/fmt/presentable/heading.rs +++ b/workspace_tests/src/fmt/presentable/heading.rs @@ -1,10 +1,10 @@ use futures::{stream, StreamExt, TryStreamExt}; use peace::{ + cli::output::{CliColorizeOpt, CliMdPresenter}, fmt::{ presentable::{CodeInline, Heading, HeadingLevel}, Presentable, }, - rt_model::output::{CliColorizeOpt, CliMdPresenter}, }; use crate::fmt::cli_output; diff --git a/workspace_tests/src/fmt/presentable/list_bulleted.rs b/workspace_tests/src/fmt/presentable/list_bulleted.rs index 13ac3b2f4..54a926e34 100644 --- a/workspace_tests/src/fmt/presentable/list_bulleted.rs +++ b/workspace_tests/src/fmt/presentable/list_bulleted.rs @@ -1,6 +1,6 @@ use peace::{ + cli::output::{CliColorizeOpt, CliMdPresenter}, fmt::{presentable::ListBulleted, Presentable}, - rt_model::output::{CliColorizeOpt, CliMdPresenter}, }; use crate::fmt::cli_output; diff --git a/workspace_tests/src/fmt/presentable/list_bulleted_aligned.rs b/workspace_tests/src/fmt/presentable/list_bulleted_aligned.rs index 40d56b8b7..8e4bea10d 100644 --- a/workspace_tests/src/fmt/presentable/list_bulleted_aligned.rs +++ b/workspace_tests/src/fmt/presentable/list_bulleted_aligned.rs @@ -1,9 +1,9 @@ use peace::{ + cli::output::{CliColorizeOpt, CliMdPresenter}, fmt::{ presentable::{Bold, CodeInline, ListBulletedAligned}, Presentable, }, - rt_model::output::{CliColorizeOpt, CliMdPresenter}, }; use crate::fmt::cli_output; diff --git a/workspace_tests/src/fmt/presentable/list_numbered.rs b/workspace_tests/src/fmt/presentable/list_numbered.rs index 422007753..eb64dc74e 100644 --- a/workspace_tests/src/fmt/presentable/list_numbered.rs +++ b/workspace_tests/src/fmt/presentable/list_numbered.rs @@ -1,6 +1,6 @@ use peace::{ + cli::output::{CliColorizeOpt, CliMdPresenter}, fmt::{presentable::ListNumbered, Presentable}, - rt_model::output::{CliColorizeOpt, CliMdPresenter}, }; use crate::fmt::cli_output; diff --git a/workspace_tests/src/fmt/presentable/list_numbered_aligned.rs b/workspace_tests/src/fmt/presentable/list_numbered_aligned.rs index 3f4df9cfc..5fe46b0ee 100644 --- a/workspace_tests/src/fmt/presentable/list_numbered_aligned.rs +++ b/workspace_tests/src/fmt/presentable/list_numbered_aligned.rs @@ -1,9 +1,9 @@ use peace::{ + cli::output::{CliColorizeOpt, CliMdPresenter}, fmt::{ presentable::{Bold, CodeInline, ListNumberedAligned}, Presentable, }, - rt_model::output::{CliColorizeOpt, CliMdPresenter}, }; use crate::fmt::cli_output; diff --git a/workspace_tests/src/lib.rs b/workspace_tests/src/lib.rs index d1fded7e3..0f698981b 100644 --- a/workspace_tests/src/lib.rs +++ b/workspace_tests/src/lib.rs @@ -17,6 +17,7 @@ pub(crate) mod mock_item; // `peace` test modules mod cfg; +mod cli; mod cmd; mod cmd_model; mod cmd_rt; diff --git a/workspace_tests/src/rt/cmds/diff_cmd.rs b/workspace_tests/src/rt/cmds/diff_cmd.rs index 12b55487c..e7defa91f 100644 --- a/workspace_tests/src/rt/cmds/diff_cmd.rs +++ b/workspace_tests/src/rt/cmds/diff_cmd.rs @@ -1,6 +1,7 @@ use diff::{VecDiff, VecDiffType}; use peace::{ cfg::{app_name, profile, FlowId}, + cli::output::CliOutput, cmd::ctx::CmdCtx, cmd_model::CmdOutcome, params::ParamsSpec, @@ -9,10 +10,7 @@ use peace::{ StatesCurrent, StatesGoal, }, rt::cmds::{DiffCmd, StatesDiscoverCmd}, - rt_model::{ - output::{CliOutput, OutputWrite}, - Flow, ItemGraphBuilder, Workspace, WorkspaceSpec, - }, + rt_model::{output::OutputWrite, Flow, ItemGraphBuilder, Workspace, WorkspaceSpec}, }; use crate::{ diff --git a/workspace_tests/src/rt_model.rs b/workspace_tests/src/rt_model.rs index 103974bfe..4c16534ab 100644 --- a/workspace_tests/src/rt_model.rs +++ b/workspace_tests/src/rt_model.rs @@ -6,7 +6,6 @@ mod item_graph_builder; mod item_wrapper; mod native; mod outcomes; -mod output; mod states_serializer; mod storage; mod workspace_dirs_builder; diff --git a/workspace_tests/src/rt_model/output/output_format.rs b/workspace_tests/src/rt_model/output/output_format.rs deleted file mode 100644 index abd208477..000000000 --- a/workspace_tests/src/rt_model/output/output_format.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::str::FromStr; - -use peace::rt_model::output::{OutputFormat, OutputFormatParseError}; - -#[test] -fn from_str_returns_ok_for_text() { - assert_eq!(Ok(OutputFormat::Text), OutputFormat::from_str("text")) -} - -#[test] -fn from_str_returns_ok_for_yaml() { - assert_eq!(Ok(OutputFormat::Yaml), OutputFormat::from_str("yaml")) -} - -#[test] -fn from_str_returns_ok_for_json() { - assert_eq!(Ok(OutputFormat::Json), OutputFormat::from_str("json")) -} - -#[test] -fn from_str_returns_err_for_unknown_string() { - assert_eq!( - Err(OutputFormatParseError("rara".to_string())), - OutputFormat::from_str("rara") - ) -} - -#[test] -fn clone() { - let output_format = OutputFormat::Text; - let output_format_clone = output_format; - - assert_eq!(output_format, output_format_clone); -} - -#[test] -fn debug() { - let output_format = OutputFormat::Text; - - assert_eq!(r#"Text"#, format!("{output_format:?}")); -} diff --git a/workspace_tests/src/rt_model/output/output_format_parse_error.rs b/workspace_tests/src/rt_model/output/output_format_parse_error.rs deleted file mode 100644 index 59b680487..000000000 --- a/workspace_tests/src/rt_model/output/output_format_parse_error.rs +++ /dev/null @@ -1,27 +0,0 @@ -use peace::rt_model::output::OutputFormatParseError; - -#[test] -fn display_includes_text_yaml_json() { - let error = OutputFormatParseError("rara".to_string()); - - assert_eq!( - r#"Failed to parse output format from string: `"rara"`. Valid values are ["text", "yaml", "json"]"#, - format!("{error}") - ); -} - -#[test] -fn clone() { - let error = OutputFormatParseError("rara".to_string()); - #[allow(clippy::redundant_clone)] // https://github.com/rust-lang/rust-clippy/issues/9011 - let error_clone = error.clone(); - - assert_eq!(error, error_clone); -} - -#[test] -fn debug() { - let error = OutputFormatParseError("rara".to_string()); - - assert_eq!(r#"OutputFormatParseError("rara")"#, format!("{error:?}")); -} From 6cb1998b11b2640cc7031c77f5d6433e35d0446b Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Tue, 6 Feb 2024 10:33:22 +1300 Subject: [PATCH 02/38] Minor documentation correction in `CliOutput`. --- crate/cli/src/output/cli_output.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crate/cli/src/output/cli_output.rs b/crate/cli/src/output/cli_output.rs index 9aeca3491..0f8bdf5e8 100644 --- a/crate/cli/src/output/cli_output.rs +++ b/crate/cli/src/output/cli_output.rs @@ -495,9 +495,8 @@ impl Default for CliOutput { } } -/// Simple serialization implementations for now. -/// -/// See for further improvements. +/// Outputs progress and `Presentable`s in either serialized or presentable +/// form. #[async_trait(?Send)] impl OutputWrite for CliOutput where From c4380963c1b85728d29bfe9e563d2c782d5e9078 Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Tue, 6 Feb 2024 10:48:32 +1300 Subject: [PATCH 03/38] Add skeleton / scaffolding for `peace_webi` crate. --- Cargo.toml | 11 ++++++ crate/webi/Cargo.toml | 31 ++++++++++++++++ crate/webi/src/lib.rs | 3 ++ crate/webi/src/output.rs | 1 + crate/webi/src/output/webi_output.rs | 54 ++++++++++++++++++++++++++++ src/lib.rs | 2 ++ workspace_tests/Cargo.toml | 3 +- workspace_tests/src/lib.rs | 2 ++ workspace_tests/src/webi.rs | 1 + 9 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 crate/webi/Cargo.toml create mode 100644 crate/webi/src/lib.rs create mode 100644 crate/webi/src/output.rs create mode 100644 crate/webi/src/output/webi_output.rs create mode 100644 workspace_tests/src/webi.rs diff --git a/Cargo.toml b/Cargo.toml index e821bda10..60b2df0fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ peace_params = { workspace = true } peace_resources = { workspace = true } peace_rt = { workspace = true } peace_rt_model = { workspace = true } +peace_webi = { workspace = true, optional = true } [features] default = [] @@ -39,6 +40,9 @@ cli = [ "dep:peace_cli", "dep:peace_cli_model", ] +webi = [ + "dep:peace_webi", +] error_reporting = [ "dep:miette", "miette?/fancy", @@ -103,6 +107,7 @@ peace_rt_model_native = { path = "crate/rt_model_native", version = "0.0.13" } peace_rt_model_web = { path = "crate/rt_model_web", version = "0.0.13" } peace_static_check_macros = { path = "crate/static_check_macros", version = "0.0.13" } peace_value_traits = { path = "crate/value_traits", version = "0.0.13" } +peace_webi = { path = "crate/webi", version = "0.0.13" } # Item crates peace_items = { path = "items", version = "0.0.13" } @@ -117,6 +122,7 @@ peace_item_tar_x = { path = "items/tar_x", version = "0.0.13" } # This does not include examples' dependencies, because we want it to be easy for # developers to see the dependencies to create an automation tool. async-trait = "0.1.77" +axum = "0.7.4" base64 = "0.21.7" bytes = "1.5.0" cfg-if = "1.0.0" @@ -134,6 +140,10 @@ heck = "0.4.1" indexmap = "2.1.0" indicatif = "0.17.7" interruptible = "0.2.1" +leptos = { version = "0.6" } +leptos_axum = "0.6" +leptos_meta = { version = "0.6" } +leptos_router = { version = "0.6" } libc = "0.2.152" miette = "5.10.0" pretty_assertions = "1.4.0" @@ -152,6 +162,7 @@ tempfile = "3.9.0" thiserror = "1.0.56" tokio = "1.35.1" tokio-util = "0.7.10" +tower-http = "0.5.1" tynm = "0.1.9" type_reg = { version = "0.7.0", features = ["debug", "untagged", "ordered"] } url = "2.5.0" diff --git a/crate/webi/Cargo.toml b/crate/webi/Cargo.toml new file mode 100644 index 000000000..1048d6567 --- /dev/null +++ b/crate/webi/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "peace_webi" +description = "Web interface for the peace automation framework." +documentation = "https://docs.rs/peace_webi/" +version.workspace = true +authors.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true +readme.workspace = true +categories.workspace = true +keywords.workspace = true +license.workspace = true + +[lib] +doctest = true +test = false + +[dependencies] +cfg-if = { workspace = true } +peace_fmt = { workspace = true } +peace_core = { workspace = true, optional = true } +peace_rt_model_core = { workspace = true } + +[features] +default = [] +output_progress = [ + "dep:peace_core", + "peace_core/output_progress", + "peace_rt_model_core/output_progress", +] diff --git a/crate/webi/src/lib.rs b/crate/webi/src/lib.rs new file mode 100644 index 000000000..b1e3dbac1 --- /dev/null +++ b/crate/webi/src/lib.rs @@ -0,0 +1,3 @@ +//! Web interface for the peace automation framework. + +pub mod output; diff --git a/crate/webi/src/output.rs b/crate/webi/src/output.rs new file mode 100644 index 000000000..068af46cc --- /dev/null +++ b/crate/webi/src/output.rs @@ -0,0 +1 @@ +mod webi_output; diff --git a/crate/webi/src/output/webi_output.rs b/crate/webi/src/output/webi_output.rs new file mode 100644 index 000000000..5993abd28 --- /dev/null +++ b/crate/webi/src/output/webi_output.rs @@ -0,0 +1,54 @@ +use std::fmt::Debug; + +use peace_fmt::Presentable; +use peace_rt_model_core::{async_trait, output::OutputWrite}; + +cfg_if::cfg_if! { + if #[cfg(feature = "output_progress")] { + use peace_core::progress::{ + ProgressComplete, + ProgressLimit, + ProgressStatus, + ProgressTracker, + ProgressUpdate, + ProgressUpdateAndId, + }; + use peace_rt_model_core::CmdProgressTracker; + } +} + +/// An `OutputWrite` implementation that writes to web elements. +#[derive(Debug)] +pub struct WebiOutput {} + +#[async_trait(?Send)] +impl OutputWrite for WebiOutput { + #[cfg(feature = "output_progress")] + async fn progress_begin(&mut self, cmd_progress_tracker: &CmdProgressTracker) {} + + #[cfg(feature = "output_progress")] + async fn progress_update( + &mut self, + progress_tracker: &ProgressTracker, + progress_update_and_id: &ProgressUpdateAndId, + ) { + } + + #[cfg(feature = "output_progress")] + async fn progress_end(&mut self, cmd_progress_tracker: &CmdProgressTracker) {} + + async fn present

(&mut self, _presentable: P) -> Result<(), E> + where + E: std::error::Error, + P: Presentable, + { + todo!() + } + + async fn write_err(&mut self, _error: &E) -> Result<(), E> + where + E: std::error::Error, + { + todo!() + } +} diff --git a/src/lib.rs b/src/lib.rs index 56d9e2d26..8892da67d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,8 @@ pub use peace_params as params; pub use peace_resources as resources; pub use peace_rt as rt; pub use peace_rt_model as rt_model; +#[cfg(feature = "webi")] +pub use peace_webi as webi; // We still can't build with `--all-features`, even with `indicatif 0.17.4`. // diff --git a/workspace_tests/Cargo.toml b/workspace_tests/Cargo.toml index 13b14bb53..334e438f6 100644 --- a/workspace_tests/Cargo.toml +++ b/workspace_tests/Cargo.toml @@ -37,12 +37,13 @@ tokio = { workspace = true, features = ["rt", "macros"] } tynm = { workspace = true } [features] -default = ["items", "output_in_memory"] +default = ["items", "output_in_memory", "webi"] # `peace` features error_reporting = ["peace/error_reporting"] output_in_memory = ["peace/output_in_memory"] output_progress = ["peace/output_progress", "peace_items/output_progress"] +webi = ["peace/webi"] # `peace_items` features items = [ diff --git a/workspace_tests/src/lib.rs b/workspace_tests/src/lib.rs index 0f698981b..5d961cda2 100644 --- a/workspace_tests/src/lib.rs +++ b/workspace_tests/src/lib.rs @@ -28,6 +28,8 @@ mod params; mod resources; mod rt; mod rt_model; +#[cfg(feature = "webi")] +mod webi; // `peace_items` test modules #[cfg(feature = "items")] diff --git a/workspace_tests/src/webi.rs b/workspace_tests/src/webi.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/workspace_tests/src/webi.rs @@ -0,0 +1 @@ + From c84a2a7dd38836f4284eedaac90fc832a91934b4 Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Tue, 6 Feb 2024 12:00:12 +1300 Subject: [PATCH 04/38] Add `peace_web_model` crate. --- Cargo.toml | 3 +++ crate/webi_model/Cargo.toml | 24 ++++++++++++++++++++++++ crate/webi_model/src/lib.rs | 5 +++++ crate/webi_model/src/webi_error.rs | 23 +++++++++++++++++++++++ src/lib.rs | 2 ++ 5 files changed, 57 insertions(+) create mode 100644 crate/webi_model/Cargo.toml create mode 100644 crate/webi_model/src/lib.rs create mode 100644 crate/webi_model/src/webi_error.rs diff --git a/Cargo.toml b/Cargo.toml index 60b2df0fd..f5482f7f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ peace_resources = { workspace = true } peace_rt = { workspace = true } peace_rt_model = { workspace = true } peace_webi = { workspace = true, optional = true } +peace_webi_model = { workspace = true, optional = true } [features] default = [] @@ -42,6 +43,7 @@ cli = [ ] webi = [ "dep:peace_webi", + "dep:peace_webi_model", ] error_reporting = [ "dep:miette", @@ -108,6 +110,7 @@ peace_rt_model_web = { path = "crate/rt_model_web", version = "0.0.13" } peace_static_check_macros = { path = "crate/static_check_macros", version = "0.0.13" } peace_value_traits = { path = "crate/value_traits", version = "0.0.13" } peace_webi = { path = "crate/webi", version = "0.0.13" } +peace_webi_model = { path = "crate/webi_model", version = "0.0.13" } # Item crates peace_items = { path = "items", version = "0.0.13" } diff --git a/crate/webi_model/Cargo.toml b/crate/webi_model/Cargo.toml new file mode 100644 index 000000000..bc9987ac4 --- /dev/null +++ b/crate/webi_model/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "peace_webi_model" +description = "Web interface data types for the peace automation framework." +documentation = "https://docs.rs/peace_webi/" +version.workspace = true +authors.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true +readme.workspace = true +categories.workspace = true +keywords.workspace = true +license.workspace = true + +[lib] +doctest = true +test = false + +[dependencies] +miette = { workspace = true, optional = true } +thiserror = { workspace = true } + +[features] +error_reporting = ["dep:miette"] diff --git a/crate/webi_model/src/lib.rs b/crate/webi_model/src/lib.rs new file mode 100644 index 000000000..61b508775 --- /dev/null +++ b/crate/webi_model/src/lib.rs @@ -0,0 +1,5 @@ +//! Web interface data types for the peace automation framework. + +pub use crate::webi_error::WebiError; + +mod webi_error; diff --git a/crate/webi_model/src/webi_error.rs b/crate/webi_model/src/webi_error.rs new file mode 100644 index 000000000..0faed2a51 --- /dev/null +++ b/crate/webi_model/src/webi_error.rs @@ -0,0 +1,23 @@ +use std::net::SocketAddr; + +/// Errors concerning the web interface. +#[cfg_attr(feature = "error_reporting", derive(miette::Diagnostic))] +#[derive(Debug, thiserror::Error)] +pub enum WebiError { + /// Failed to start web server for Web interface. + #[error("Failed to start web server for Web interface on socket: {socket_addr}")] + #[cfg_attr( + feature = "error_reporting", + diagnostic( + code(peace_webi_model::webi_axum_serve), + help("Another process may be using the same socket address: {socket_addr}.") + ) + )] + ServerServe { + /// The socket address that the web server attempted to listen on. + socket_addr: SocketAddr, + /// Underlying error. + #[source] + error: std::io::Error, + }, +} diff --git a/src/lib.rs b/src/lib.rs index 8892da67d..893c97bae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,8 @@ pub use peace_rt as rt; pub use peace_rt_model as rt_model; #[cfg(feature = "webi")] pub use peace_webi as webi; +#[cfg(feature = "webi")] +pub use peace_webi_model as webi_model; // We still can't build with `--all-features`, even with `indicatif 0.17.4`. // From 9fcb806a409ed46d09b4f0d264b16e3202556e89 Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Sat, 10 Feb 2024 17:16:22 +1300 Subject: [PATCH 05/38] Add skeleton web interface components. --- crate/webi/Cargo.toml | 11 +++- crate/webi/src/components.rs | 8 +++ crate/webi/src/components/flow_graph.rs | 61 +++++++++++++++++++++ crate/webi/src/components/home.rs | 31 +++++++++++ crate/webi/src/lib.rs | 1 + crate/webi/src/output.rs | 4 ++ crate/webi/src/output/webi_output.rs | 73 ++++++++++++++++++++++--- 7 files changed, 181 insertions(+), 8 deletions(-) create mode 100644 crate/webi/src/components.rs create mode 100644 crate/webi/src/components/flow_graph.rs create mode 100644 crate/webi/src/components/home.rs diff --git a/crate/webi/Cargo.toml b/crate/webi/Cargo.toml index 1048d6567..290b2b950 100644 --- a/crate/webi/Cargo.toml +++ b/crate/webi/Cargo.toml @@ -17,10 +17,19 @@ doctest = true test = false [dependencies] +axum = { workspace = true } cfg-if = { workspace = true } -peace_fmt = { workspace = true } +leptos = { workspace = true } +leptos_axum = { workspace = true } +leptos_meta = { workspace = true } +leptos_router = { workspace = true } peace_core = { workspace = true, optional = true } +peace_fmt = { workspace = true } peace_rt_model_core = { workspace = true } +peace_value_traits = { workspace = true } +peace_webi_model = { workspace = true } +tokio = { workspace = true, features = ["net"] } +tower-http = { workspace = true, features = ["fs"] } [features] default = [] diff --git a/crate/webi/src/components.rs b/crate/webi/src/components.rs new file mode 100644 index 000000000..a7d2bb61e --- /dev/null +++ b/crate/webi/src/components.rs @@ -0,0 +1,8 @@ +#![allow(non_snake_case)] // Components are all PascalCase. + +//! Components for rendering a flow. + +pub use self::{flow_graph::FlowGraph, home::Home}; + +mod flow_graph; +mod home; diff --git a/crate/webi/src/components/flow_graph.rs b/crate/webi/src/components/flow_graph.rs new file mode 100644 index 000000000..373a487a5 --- /dev/null +++ b/crate/webi/src/components/flow_graph.rs @@ -0,0 +1,61 @@ +use leptos::{ + component, create_signal, server_fn::error::NoCustomError, view, IntoView, ServerFnError, + SignalGet, SignalUpdate, Transition, +}; + +/// Renders the flow graph. +#[component] +pub fn FlowGraph() -> impl IntoView { + let (count, set_count) = create_signal(0); + + let dot_source_resource = leptos::create_resource( + || (), + move |()| async move { flow_graph_src().await.unwrap() }, + ); + let dot_source_result = { + move || { + let dot_source = dot_source_resource + .get() + .unwrap_or_else(|| String::from("digraph { a -> b; }")); + + let script_src = format!( + "\ + import {{ Graphviz }} from \"https://cdn.jsdelivr.net/npm/@hpcc-js/wasm/dist/graphviz.js\";\n\ + \n\ + const graphviz = await Graphviz.load();\n\ + const dot_source = `{dot_source}`;\n\ + document.getElementById(\"flow_dot_diagram\").innerHTML =\n\ + graphviz.layout(dot_source, \"svg\", \"dot\");\n\ + " + ); + + view! { + + } + } + }; + + view! { +

+
+
+
+ "hello leptos!" { move || count.get() } +
+ +
+
+ "Loading graph..."

}> + { dot_source_result } +
+ } +} + +#[leptos::server(endpoint = "/flow_graph")] +pub async fn flow_graph_src() -> Result> { + Ok(String::from("digraph { a; b; c; a -> c; b -> c; }")) +} diff --git a/crate/webi/src/components/home.rs b/crate/webi/src/components/home.rs new file mode 100644 index 000000000..647840325 --- /dev/null +++ b/crate/webi/src/components/home.rs @@ -0,0 +1,31 @@ +use leptos::{component, view, IntoView}; +use leptos_meta::{provide_meta_context, Link, Stylesheet}; +use leptos_router::{Route, Router, Routes}; + +use crate::components::FlowGraph; + +#[component] +pub fn Home() -> impl IntoView { + // Provides context that manages stylesheets, titles, meta tags, etc. + provide_meta_context(); + + let site_prefix = option_env!("SITE_PREFIX").unwrap_or(""); + let favicon_path = format!("{site_prefix}/favicon.ico"); + let stylesheet_path = format!("{site_prefix}/pkg/peace_webi.css"); + let fonts_path = format!("{site_prefix}/fonts/fonts.css"); + + view! { + + + + +
+ + + }/> + +
+
+ } +} diff --git a/crate/webi/src/lib.rs b/crate/webi/src/lib.rs index b1e3dbac1..812e6305c 100644 --- a/crate/webi/src/lib.rs +++ b/crate/webi/src/lib.rs @@ -1,3 +1,4 @@ //! Web interface for the peace automation framework. +pub mod components; pub mod output; diff --git a/crate/webi/src/output.rs b/crate/webi/src/output.rs index 068af46cc..064e4584d 100644 --- a/crate/webi/src/output.rs +++ b/crate/webi/src/output.rs @@ -1 +1,5 @@ +//! Web interface output types. + +pub use self::webi_output::WebiOutput; + mod webi_output; diff --git a/crate/webi/src/output/webi_output.rs b/crate/webi/src/output/webi_output.rs index 5993abd28..e3b90c9a1 100644 --- a/crate/webi/src/output/webi_output.rs +++ b/crate/webi/src/output/webi_output.rs @@ -1,7 +1,16 @@ -use std::fmt::Debug; +use std::{fmt::Debug, net::SocketAddr, path::PathBuf}; +use axum::Router; +use leptos::view; +use leptos_axum::LeptosRoutes; use peace_fmt::Presentable; use peace_rt_model_core::{async_trait, output::OutputWrite}; +use peace_value_traits::AppError; +use peace_webi_model::WebiError; +use tokio::io::AsyncWriteExt; +use tower_http::services::ServeDir; + +use crate::components::Home; cfg_if::cfg_if! { if #[cfg(feature = "output_progress")] { @@ -19,10 +28,60 @@ cfg_if::cfg_if! { /// An `OutputWrite` implementation that writes to web elements. #[derive(Debug)] -pub struct WebiOutput {} +pub struct WebiOutput { + /// IP address and port to listen on. + socket_addr: Option, +} + +impl WebiOutput { + pub async fn start(&self) -> Result<(), WebiError> { + let Self { socket_addr } = self; + + // Setting this to None means we'll be using cargo-leptos and its env vars + let conf = leptos::get_configuration(None).await.unwrap(); + let leptos_options = conf.leptos_options; + let socket_addr = socket_addr.unwrap_or(leptos_options.site_addr); + let routes = leptos_axum::generate_route_list(|| view! { }); + + let router = Router::new() + // serve the pkg directory + .nest_service( + "/pkg", + ServeDir::new(PathBuf::from_iter([ + leptos_options.site_root.as_str(), + leptos_options.site_pkg_dir.as_str(), + ])), + ) + // serve the SSR rendered homepage + .leptos_routes(&leptos_options, routes, move || view! { }) + .with_state(leptos_options); + + let listener = tokio::net::TcpListener::bind(socket_addr) + .await + .unwrap_or_else(|e| panic!("Failed to listen on {socket_addr}. Error: {e}")); + let (Ok(()) | Err(_)) = tokio::io::stderr() + .write_all(format!("listening on http://{}\n", socket_addr).as_bytes()) + .await; + let (Ok(()) | Err(_)) = tokio::io::stderr() + .write_all( + format!( + "working dir: {}\n", + std::env::current_dir().unwrap().display() + ) + .as_bytes(), + ) + .await; + axum::serve(listener, router) + .await + .map_err(|error| WebiError::ServerServe { socket_addr, error }) + } +} #[async_trait(?Send)] -impl OutputWrite for WebiOutput { +impl OutputWrite for WebiOutput +where + AppErrorT: AppError, +{ #[cfg(feature = "output_progress")] async fn progress_begin(&mut self, cmd_progress_tracker: &CmdProgressTracker) {} @@ -37,17 +96,17 @@ impl OutputWrite for WebiOutput { #[cfg(feature = "output_progress")] async fn progress_end(&mut self, cmd_progress_tracker: &CmdProgressTracker) {} - async fn present

(&mut self, _presentable: P) -> Result<(), E> + async fn present

(&mut self, _presentable: P) -> Result<(), AppErrorT> where - E: std::error::Error, + AppErrorT: std::error::Error, P: Presentable, { todo!() } - async fn write_err(&mut self, _error: &E) -> Result<(), E> + async fn write_err(&mut self, _error: &AppErrorT) -> Result<(), AppErrorT> where - E: std::error::Error, + AppErrorT: std::error::Error, { todo!() } From 4fa7425717268cd3ef060a94a4c4d5455b59aa98 Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Thu, 8 Feb 2024 18:41:19 +1300 Subject: [PATCH 06/38] Get `envman` to use `webi` for leptos and cli with web server builds. --- Cargo.toml | 10 ++++ crate/webi/Cargo.toml | 23 ++++----- crate/webi/src/components.rs | 8 ---- crate/webi/src/lib.rs | 6 ++- crate/webi/src/output.rs | 5 -- crate/webi_components/Cargo.toml | 30 ++++++++++++ .../src}/flow_graph.rs | 0 .../src}/home.rs | 2 +- crate/webi_components/src/lib.rs | 8 ++++ crate/webi_output/Cargo.toml | 47 +++++++++++++++++++ crate/webi_output/src/lib.rs | 5 ++ .../output => webi_output/src}/webi_output.rs | 25 ++++++---- examples/envman/Cargo.toml | 13 +++-- examples/envman/src/lib.rs | 2 +- examples/envman/src/main.rs | 18 ++++--- examples/envman/src/main_cli.rs | 10 ++-- examples/envman/src/model.rs | 2 +- examples/envman/src/model/envman_error.rs | 12 +++++ src/lib.rs | 2 + 19 files changed, 172 insertions(+), 56 deletions(-) delete mode 100644 crate/webi/src/components.rs delete mode 100644 crate/webi/src/output.rs create mode 100644 crate/webi_components/Cargo.toml rename crate/{webi/src/components => webi_components/src}/flow_graph.rs (100%) rename crate/{webi/src/components => webi_components/src}/home.rs (96%) create mode 100644 crate/webi_components/src/lib.rs create mode 100644 crate/webi_output/Cargo.toml create mode 100644 crate/webi_output/src/lib.rs rename crate/{webi/src/output => webi_output/src}/webi_output.rs (85%) diff --git a/Cargo.toml b/Cargo.toml index f5482f7f0..f6f01a246 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ peace_resources = { workspace = true } peace_rt = { workspace = true } peace_rt_model = { workspace = true } peace_webi = { workspace = true, optional = true } +peace_webi_components = { workspace = true, optional = true } peace_webi_model = { workspace = true, optional = true } [features] @@ -43,6 +44,7 @@ cli = [ ] webi = [ "dep:peace_webi", + "dep:peace_webi_components", "dep:peace_webi_model", ] error_reporting = [ @@ -53,6 +55,7 @@ error_reporting = [ "peace_params/error_reporting", "peace_rt/error_reporting", "peace_rt_model/error_reporting", + "peace_webi_model?/error_reporting", ] output_in_memory = ["peace_cli?/output_in_memory"] output_progress = [ @@ -61,6 +64,11 @@ output_progress = [ "peace_cfg/output_progress", "peace_rt/output_progress", "peace_rt_model/output_progress", + "peace_webi?/output_progress", +] +ssr = [ + "peace_webi?/ssr", + "peace_webi_components?/ssr", ] [workspace] @@ -110,7 +118,9 @@ peace_rt_model_web = { path = "crate/rt_model_web", version = "0.0.13" } peace_static_check_macros = { path = "crate/static_check_macros", version = "0.0.13" } peace_value_traits = { path = "crate/value_traits", version = "0.0.13" } peace_webi = { path = "crate/webi", version = "0.0.13" } +peace_webi_components = { path = "crate/webi_components", version = "0.0.13" } peace_webi_model = { path = "crate/webi_model", version = "0.0.13" } +peace_webi_output = { path = "crate/webi_output", version = "0.0.13" } # Item crates peace_items = { path = "items", version = "0.0.13" } diff --git a/crate/webi/Cargo.toml b/crate/webi/Cargo.toml index 290b2b950..bc7ec5c90 100644 --- a/crate/webi/Cargo.toml +++ b/crate/webi/Cargo.toml @@ -17,24 +17,17 @@ doctest = true test = false [dependencies] -axum = { workspace = true } -cfg-if = { workspace = true } -leptos = { workspace = true } -leptos_axum = { workspace = true } -leptos_meta = { workspace = true } -leptos_router = { workspace = true } -peace_core = { workspace = true, optional = true } -peace_fmt = { workspace = true } -peace_rt_model_core = { workspace = true } -peace_value_traits = { workspace = true } +peace_webi_components = { workspace = true } peace_webi_model = { workspace = true } -tokio = { workspace = true, features = ["net"] } -tower-http = { workspace = true, features = ["fs"] } +peace_webi_output = { workspace = true, optional = true } [features] default = [] output_progress = [ - "dep:peace_core", - "peace_core/output_progress", - "peace_rt_model_core/output_progress", + "peace_webi_output?/output_progress", +] +ssr = [ + "dep:peace_webi_output", + "peace_webi_output/ssr", + "peace_webi_components/ssr", ] diff --git a/crate/webi/src/components.rs b/crate/webi/src/components.rs deleted file mode 100644 index a7d2bb61e..000000000 --- a/crate/webi/src/components.rs +++ /dev/null @@ -1,8 +0,0 @@ -#![allow(non_snake_case)] // Components are all PascalCase. - -//! Components for rendering a flow. - -pub use self::{flow_graph::FlowGraph, home::Home}; - -mod flow_graph; -mod home; diff --git a/crate/webi/src/lib.rs b/crate/webi/src/lib.rs index 812e6305c..3d12dad1a 100644 --- a/crate/webi/src/lib.rs +++ b/crate/webi/src/lib.rs @@ -1,4 +1,6 @@ //! Web interface for the peace automation framework. -pub mod components; -pub mod output; +pub use peace_webi_components as components; +pub use peace_webi_model as model; +#[cfg(feature = "ssr")] +pub use peace_webi_output as output; diff --git a/crate/webi/src/output.rs b/crate/webi/src/output.rs deleted file mode 100644 index 064e4584d..000000000 --- a/crate/webi/src/output.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Web interface output types. - -pub use self::webi_output::WebiOutput; - -mod webi_output; diff --git a/crate/webi_components/Cargo.toml b/crate/webi_components/Cargo.toml new file mode 100644 index 000000000..83e4cb2a1 --- /dev/null +++ b/crate/webi_components/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "peace_webi_components" +description = "Web interface components for the peace automation framework." +documentation = "https://docs.rs/peace_webi_components/" +version.workspace = true +authors.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true +readme.workspace = true +categories.workspace = true +keywords.workspace = true +license.workspace = true + +[lib] +doctest = true +test = false + +[dependencies] +leptos = { workspace = true } +leptos_meta = { workspace = true } +leptos_router = { workspace = true } + +[features] +default = [] +ssr = [ + "leptos/ssr", + "leptos_meta/ssr", + "leptos_router/ssr", +] diff --git a/crate/webi/src/components/flow_graph.rs b/crate/webi_components/src/flow_graph.rs similarity index 100% rename from crate/webi/src/components/flow_graph.rs rename to crate/webi_components/src/flow_graph.rs diff --git a/crate/webi/src/components/home.rs b/crate/webi_components/src/home.rs similarity index 96% rename from crate/webi/src/components/home.rs rename to crate/webi_components/src/home.rs index 647840325..a8b824b94 100644 --- a/crate/webi/src/components/home.rs +++ b/crate/webi_components/src/home.rs @@ -2,7 +2,7 @@ use leptos::{component, view, IntoView}; use leptos_meta::{provide_meta_context, Link, Stylesheet}; use leptos_router::{Route, Router, Routes}; -use crate::components::FlowGraph; +use crate::FlowGraph; #[component] pub fn Home() -> impl IntoView { diff --git a/crate/webi_components/src/lib.rs b/crate/webi_components/src/lib.rs new file mode 100644 index 000000000..93ca0af32 --- /dev/null +++ b/crate/webi_components/src/lib.rs @@ -0,0 +1,8 @@ +#![allow(non_snake_case)] // Components are all PascalCase. + +//! Web interface components for the peace automation framework. + +pub use crate::{flow_graph::FlowGraph, home::Home}; + +mod flow_graph; +mod home; diff --git a/crate/webi_output/Cargo.toml b/crate/webi_output/Cargo.toml new file mode 100644 index 000000000..7712c5c41 --- /dev/null +++ b/crate/webi_output/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "peace_webi_output" +description = "Web interface output for the peace automation framework." +documentation = "https://docs.rs/peace_webi_output/" +version.workspace = true +authors.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true +readme.workspace = true +categories.workspace = true +keywords.workspace = true +license.workspace = true + +[lib] +doctest = true +test = false + +[dependencies] +axum = { workspace = true } +cfg-if = { workspace = true } +leptos = { workspace = true } +leptos_axum = { workspace = true } +leptos_meta = { workspace = true } +leptos_router = { workspace = true } +peace_core = { workspace = true, optional = true } +peace_fmt = { workspace = true } +peace_rt_model_core = { workspace = true } +peace_value_traits = { workspace = true } +peace_webi_components = { workspace = true } +peace_webi_model = { workspace = true } +tokio = { workspace = true, features = ["net"] } +tower-http = { workspace = true, features = ["fs"] } + +[features] +default = [] +output_progress = [ + "dep:peace_core", + "peace_core/output_progress", + "peace_rt_model_core/output_progress", +] +ssr = [ + "leptos/ssr", + "leptos_meta/ssr", + "leptos_router/ssr", + "peace_webi_components/ssr", +] diff --git a/crate/webi_output/src/lib.rs b/crate/webi_output/src/lib.rs new file mode 100644 index 000000000..9131e0a9a --- /dev/null +++ b/crate/webi_output/src/lib.rs @@ -0,0 +1,5 @@ +//! Web interface output for the peace automation framework. + +pub use crate::webi_output::WebiOutput; + +mod webi_output; diff --git a/crate/webi/src/output/webi_output.rs b/crate/webi_output/src/webi_output.rs similarity index 85% rename from crate/webi/src/output/webi_output.rs rename to crate/webi_output/src/webi_output.rs index e3b90c9a1..c8d68fa3f 100644 --- a/crate/webi/src/output/webi_output.rs +++ b/crate/webi_output/src/webi_output.rs @@ -6,20 +6,19 @@ use leptos_axum::LeptosRoutes; use peace_fmt::Presentable; use peace_rt_model_core::{async_trait, output::OutputWrite}; use peace_value_traits::AppError; +use peace_webi_components::Home; use peace_webi_model::WebiError; use tokio::io::AsyncWriteExt; use tower_http::services::ServeDir; -use crate::components::Home; - cfg_if::cfg_if! { if #[cfg(feature = "output_progress")] { use peace_core::progress::{ - ProgressComplete, - ProgressLimit, - ProgressStatus, + // ProgressComplete, + // ProgressLimit, + // ProgressStatus, ProgressTracker, - ProgressUpdate, + // ProgressUpdate, ProgressUpdateAndId, }; use peace_rt_model_core::CmdProgressTracker; @@ -33,6 +32,12 @@ pub struct WebiOutput { socket_addr: Option, } +impl WebiOutput { + pub fn new(socket_addr: Option) -> Self { + Self { socket_addr } + } +} + impl WebiOutput { pub async fn start(&self) -> Result<(), WebiError> { let Self { socket_addr } = self; @@ -83,18 +88,18 @@ where AppErrorT: AppError, { #[cfg(feature = "output_progress")] - async fn progress_begin(&mut self, cmd_progress_tracker: &CmdProgressTracker) {} + async fn progress_begin(&mut self, _cmd_progress_tracker: &CmdProgressTracker) {} #[cfg(feature = "output_progress")] async fn progress_update( &mut self, - progress_tracker: &ProgressTracker, - progress_update_and_id: &ProgressUpdateAndId, + _progress_tracker: &ProgressTracker, + _progress_update_and_id: &ProgressUpdateAndId, ) { } #[cfg(feature = "output_progress")] - async fn progress_end(&mut self, cmd_progress_tracker: &CmdProgressTracker) {} + async fn progress_end(&mut self, _cmd_progress_tracker: &CmdProgressTracker) {} async fn present

(&mut self, _presentable: P) -> Result<(), AppErrorT> where diff --git a/examples/envman/Cargo.toml b/examples/envman/Cargo.toml index 073d44814..f7b31ac4c 100644 --- a/examples/envman/Cargo.toml +++ b/examples/envman/Cargo.toml @@ -28,7 +28,7 @@ chrono = { version = "0.4.33", default-features = false, features = ["clock", "s derivative = { version = "2.2.0", optional = true } futures = { version = "0.3.30", optional = true } md5-rs = { version = "0.1.5", optional = true } # WASM compatible, and reads bytes as stream -peace = { path = "../..", default-features = false, features = ["cli"] } +peace = { path = "../..", default-features = false } peace_items = { path = "../../items", features = ["file_download", "tar_x"] } semver = { version = "1.0.21", optional = true } serde = { version = "1.0.196", features = ["derive"] } @@ -71,9 +71,10 @@ default = [] cli = [ "error_reporting", "output_progress", - "flow_logic", "web_server", + + "peace/cli", ] # The `"ssr"` feature is used for two purposes: # @@ -99,8 +100,13 @@ ssr = [ "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", + "peace/webi", + "peace/ssr", +] +csr = [ + "web_components", + "peace/webi", ] -csr = ["web_components"] # === peace passthrough features === # error_reporting = [ @@ -145,6 +151,7 @@ hydrate = [ "leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate", + "peace/webi", ] [package.metadata.cargo-all-features] diff --git a/examples/envman/src/lib.rs b/examples/envman/src/lib.rs index 31ea3e639..99de5dcd9 100644 --- a/examples/envman/src/lib.rs +++ b/examples/envman/src/lib.rs @@ -69,7 +69,7 @@ cfg_if::cfg_if! { use wasm_bindgen::prelude::wasm_bindgen; use leptos::*; - use crate::web::components::Home; + use peace::webi_components::Home; #[wasm_bindgen] pub async fn hydrate() { diff --git a/examples/envman/src/main.rs b/examples/envman/src/main.rs index 552b5f679..8114700a3 100644 --- a/examples/envman/src/main.rs +++ b/examples/envman/src/main.rs @@ -33,10 +33,8 @@ cfg_if::cfg_if! { } } else if #[cfg(feature = "ssr")] { // web server - use envman::{ - model::EnvManError, - web::WebServer, - }; + use envman::model::EnvManError; + use peace::webi::output::WebiOutput; #[cfg(not(feature = "error_reporting"))] pub fn main() -> Result<(), EnvManError> { @@ -48,7 +46,11 @@ cfg_if::cfg_if! { .build() .map_err(EnvManError::TokioRuntimeInit)?; - runtime.block_on(WebServer::start(None)) + let webi_output = WebiOutput::new(None); + + runtime.block_on(async move { + webi_output.start().await.map_err(EnvManError::from) + }) } #[cfg(feature = "error_reporting")] @@ -73,8 +75,12 @@ cfg_if::cfg_if! { .build() .map_err(EnvManError::TokioRuntimeInit)?; + let webi_output = WebiOutput::new(None); + runtime - .block_on(WebServer::start(None)) + .block_on(async move { + webi_output.start().await.map_err(EnvManError::from) + }) .map_err(peace::miette::Report::from) } } else if #[cfg(feature = "csr")] { diff --git a/examples/envman/src/main_cli.rs b/examples/envman/src/main_cli.rs index 070967f8e..0c8ab6da5 100644 --- a/examples/envman/src/main_cli.rs +++ b/examples/envman/src/main_cli.rs @@ -14,9 +14,6 @@ use envman::{ use peace::cli::output::CliOutput; use tokio::io::Stdout; -#[cfg(feature = "web_server")] -use envman::web::WebServer; - pub fn run() -> Result<(), EnvManError> { let CliArgs { command, @@ -131,7 +128,12 @@ async fn run_command( EnvManCommand::Clean => EnvCleanCmd::run(cli_output, debug).await?, #[cfg(feature = "web_server")] EnvManCommand::Web { address, port } => { - WebServer::start(Some(SocketAddr::from((address, port)))).await? + use peace::webi::output::WebiOutput; + + let webi_output = WebiOutput::new(Some(SocketAddr::from((address, port)))); + webi_output.start().await?; + + // WebServer::start(Some(SocketAddr::from((address, port)))).await? } } diff --git a/examples/envman/src/model.rs b/examples/envman/src/model.rs index 0f9851f8a..475bd6265 100644 --- a/examples/envman/src/model.rs +++ b/examples/envman/src/model.rs @@ -14,7 +14,7 @@ pub use self::{ repo_slug_error::RepoSlugError, }; -#[cfg(not(target_arch = "wasm32"))] +#[cfg(feature = "cli")] pub mod cli_args; mod env_diff_selection; diff --git a/examples/envman/src/model/envman_error.rs b/examples/envman/src/model/envman_error.rs index cab776d2c..e56e3c896 100644 --- a/examples/envman/src/model/envman_error.rs +++ b/examples/envman/src/model/envman_error.rs @@ -156,6 +156,18 @@ pub enum EnvManError { TokioRuntimeInit(#[source] std::io::Error), // === Web Server errors === // + /// Web interface server produced an error. + #[cfg(feature = "ssr")] + #[error("Web interface server produced an error.")] + #[cfg_attr(feature = "error_reporting", diagnostic(code(envman::webi)))] + Webi { + /// Underlying error. + #[cfg_attr(feature = "error_reporting", diagnostic_source)] + #[from] + #[source] + error: peace::webi_model::WebiError, + }, + /// Web server ended due to an error. #[cfg(feature = "ssr")] #[error("Web server ended due to an error.")] diff --git a/src/lib.rs b/src/lib.rs index 893c97bae..566ae7ed4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,8 @@ pub use peace_rt_model as rt_model; #[cfg(feature = "webi")] pub use peace_webi as webi; #[cfg(feature = "webi")] +pub use peace_webi_components as webi_components; +#[cfg(feature = "webi")] pub use peace_webi_model as webi_model; // We still can't build with `--all-features`, even with `indicatif 0.17.4`. From d23c1dc11b3b2f7e06125de7443042eb7566531f Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Sat, 10 Feb 2024 16:20:13 +1300 Subject: [PATCH 07/38] Get assets to be served from correct path. --- crate/webi_output/src/webi_output.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crate/webi_output/src/webi_output.rs b/crate/webi_output/src/webi_output.rs index c8d68fa3f..1fb5a7cec 100644 --- a/crate/webi_output/src/webi_output.rs +++ b/crate/webi_output/src/webi_output.rs @@ -52,10 +52,7 @@ impl WebiOutput { // serve the pkg directory .nest_service( "/pkg", - ServeDir::new(PathBuf::from_iter([ - leptos_options.site_root.as_str(), - leptos_options.site_pkg_dir.as_str(), - ])), + ServeDir::new(PathBuf::from(leptos_options.site_pkg_dir.as_str())), ) // serve the SSR rendered homepage .leptos_routes(&leptos_options, routes, move || view! { }) From 7fb1bd56fe793d3276bde755c47f095e526af371 Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Sat, 10 Feb 2024 17:09:20 +1300 Subject: [PATCH 08/38] Get `trunk` to build `envman`. --- .gitignore | 1 + DEVELOPMENT.md | 8 +++++++- examples/envman/Cargo.toml | 2 +- examples/envman/index.html | 11 +++++++++++ 4 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 examples/envman/index.html diff --git a/.gitignore b/.gitignore index 96ef6c0b9..75bb17cbc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ +dist /target Cargo.lock diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index f0091076e..f3d847478 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -135,9 +135,15 @@ These instructions are for Linux. They may work on OS X, but for Windows, please Build and serve the `envman` example: ```bash -cargo leptos serve --project "envman" -v +cargo leptos watch --project "envman" -v ``` +You can also use `trunk` to build the client side `csr` app. + +```bash +(cd examples/envman && trunk build -d ../../dist) +```` + ### Uninstallation To uninstall web tooling: diff --git a/examples/envman/Cargo.toml b/examples/envman/Cargo.toml index f7b31ac4c..d639935be 100644 --- a/examples/envman/Cargo.toml +++ b/examples/envman/Cargo.toml @@ -15,7 +15,7 @@ test = false [lib] doctest = false test = false -crate-type = ["cdylib", "rlib"] +crate-type = ["cdylib"] [dependencies] aws-config = { version = "1.1.4", optional = true } diff --git a/examples/envman/index.html b/examples/envman/index.html new file mode 100644 index 000000000..9972a710b --- /dev/null +++ b/examples/envman/index.html @@ -0,0 +1,11 @@ + + + + + + + From b42b7eaec0e16b91265508395cffd45e7bee41c8 Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Sun, 11 Feb 2024 11:48:27 +1300 Subject: [PATCH 09/38] Serve assets from `WebiOutput` by embedding them in the library. --- DEVELOPMENT.md | 18 ++++-- crate/webi_components/src/home.rs | 6 +- crate/webi_model/src/webi_error.rs | 36 +++++++++++- crate/webi_output/Cargo.toml | 1 + crate/webi_output/src/assets.rs | 52 ++++++++++++++++++ crate/webi_output/src/assets/favicon.ico | Bin 0 -> 15406 bytes crate/webi_output/src/assets/fonts/fonts.css | 33 +++++++++++ .../LiberationMono-Bold-webfont.woff | Bin 0 -> 20992 bytes .../LiberationMono-BoldItalic-webfont.woff | Bin 0 -> 22244 bytes .../LiberationMono-Italic-webfont.woff | Bin 0 -> 22084 bytes .../LiberationMono-Regular-webfont.woff | Bin 0 -> 20764 bytes .../liberationmono/SIL Open Font License.txt | 46 ++++++++++++++++ crate/webi_output/src/lib.rs | 2 + crate/webi_output/src/webi_output.rs | 31 ++++++++++- examples/envman/Cargo.toml | 2 +- 15 files changed, 214 insertions(+), 13 deletions(-) create mode 100644 crate/webi_output/src/assets.rs create mode 100644 crate/webi_output/src/assets/favicon.ico create mode 100644 crate/webi_output/src/assets/fonts/fonts.css create mode 100644 crate/webi_output/src/assets/fonts/liberationmono/LiberationMono-Bold-webfont.woff create mode 100644 crate/webi_output/src/assets/fonts/liberationmono/LiberationMono-BoldItalic-webfont.woff create mode 100644 crate/webi_output/src/assets/fonts/liberationmono/LiberationMono-Italic-webfont.woff create mode 100644 crate/webi_output/src/assets/fonts/liberationmono/LiberationMono-Regular-webfont.woff create mode 100644 crate/webi_output/src/assets/fonts/liberationmono/SIL Open Font License.txt diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index f3d847478..4592697c3 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -132,17 +132,27 @@ These instructions are for Linux. They may work on OS X, but for Windows, please > ℹ️ These commands assume you are running them from the repository root directory. -Build and serve the `envman` example: +Build and serve the `envman` example. Either: ```bash +# Watch, build, and `envman` purely as a web server. cargo leptos watch --project "envman" -v ``` -You can also use `trunk` to build the client side `csr` app. +Or: ```bash -(cd examples/envman && trunk build -d ../../dist) -```` +# Build `envman` as a cli tool. +cargo +stable leptos build --project envman --bin-features "cli" \ + && test -d /tmp/demo \ + || mkdir /tmp/demo \ + && cp -R target/{debug/envman,web/envman/pkg} /tmp/demo + +# In a separate terminal +cd /tmp/demo +./envman web +``` + ### Uninstallation diff --git a/crate/webi_components/src/home.rs b/crate/webi_components/src/home.rs index a8b824b94..61cd5e089 100644 --- a/crate/webi_components/src/home.rs +++ b/crate/webi_components/src/home.rs @@ -10,13 +10,11 @@ pub fn Home() -> impl IntoView { provide_meta_context(); let site_prefix = option_env!("SITE_PREFIX").unwrap_or(""); - let favicon_path = format!("{site_prefix}/favicon.ico"); - let stylesheet_path = format!("{site_prefix}/pkg/peace_webi.css"); - let fonts_path = format!("{site_prefix}/fonts/fonts.css"); + let favicon_path = format!("{site_prefix}/webi/favicon.ico"); + let fonts_path = format!("{site_prefix}/webi/fonts/fonts.css"); view! { -

diff --git a/crate/webi_model/src/webi_error.rs b/crate/webi_model/src/webi_error.rs index 0faed2a51..80b930107 100644 --- a/crate/webi_model/src/webi_error.rs +++ b/crate/webi_model/src/webi_error.rs @@ -1,16 +1,48 @@ -use std::net::SocketAddr; +use std::{net::SocketAddr, path::PathBuf}; /// Errors concerning the web interface. #[cfg_attr(feature = "error_reporting", derive(miette::Diagnostic))] #[derive(Debug, thiserror::Error)] pub enum WebiError { + /// Failed to create asset directory. + #[error("Failed to create asset directory: `{asset_dir}`")] + #[cfg_attr( + feature = "error_reporting", + diagnostic( + code(peace_webi_model::webi_asset_dir_create), + help("Check if you have sufficient permission to write to the directory.") + ) + )] + AssetDirCreate { + /// The directory attempted to be created. + asset_dir: PathBuf, + /// The underlying error. + error: std::io::Error, + }, + + /// Failed to create asset directory. + #[error("Failed to write asset: `{asset_path}`")] + #[cfg_attr( + feature = "error_reporting", + diagnostic( + code(peace_webi_model::webi_asset_write), + help("Check if you have sufficient permission to write to the file.") + ) + )] + AssetWrite { + /// Path to the file attempted to be written to. + asset_path: PathBuf, + /// The underlying error. + error: std::io::Error, + }, + /// Failed to start web server for Web interface. #[error("Failed to start web server for Web interface on socket: {socket_addr}")] #[cfg_attr( feature = "error_reporting", diagnostic( code(peace_webi_model::webi_axum_serve), - help("Another process may be using the same socket address: {socket_addr}.") + help("Another process may be using the same socket address.") ) )] ServerServe { diff --git a/crate/webi_output/Cargo.toml b/crate/webi_output/Cargo.toml index 7712c5c41..1a71369ff 100644 --- a/crate/webi_output/Cargo.toml +++ b/crate/webi_output/Cargo.toml @@ -19,6 +19,7 @@ test = false [dependencies] axum = { workspace = true } cfg-if = { workspace = true } +futures = { workspace = true } leptos = { workspace = true } leptos_axum = { workspace = true } leptos_meta = { workspace = true } diff --git a/crate/webi_output/src/assets.rs b/crate/webi_output/src/assets.rs new file mode 100644 index 000000000..815da7779 --- /dev/null +++ b/crate/webi_output/src/assets.rs @@ -0,0 +1,52 @@ +//! Assets to include with the shipped binary, but we can't get it bundled +//! automatically with either `trunk` or `cargo-leptos`. So we include the +//! bytes. + +/// Styles shipped with `peace_webi_output`. +pub const PEACE_FAVICON_ICO: &[u8] = include_bytes!("assets/favicon.ico"); + +/// Provides CSS `@font-face` definitions for the following font families: +/// +/// * liberationmono +/// * liberationmono-bold +/// * liberationmono-italic +/// * liberationmono-bold-italic +pub const FONTS_LIBERATION_MONO_CSS_FONT_FACES: &[u8] = include_bytes!("assets/fonts/fonts.css"); + +/// The Liberation Mono Regular font bytes. +pub const FONTS_LIBERATION_MONO_REGULAR: &[u8] = + include_bytes!("assets/fonts/liberationmono/LiberationMono-Regular-webfont.woff"); + +/// The Liberation Mono Bold font bytes. +pub const FONTS_LIBERATION_MONO_BOLD: &[u8] = + include_bytes!("assets/fonts/liberationmono/LiberationMono-Bold-webfont.woff"); + +/// The Liberation Mono Italic font bytes. +pub const FONTS_LIBERATION_MONO_ITALIC: &[u8] = + include_bytes!("assets/fonts/liberationmono/LiberationMono-Italic-webfont.woff"); + +/// The Liberation Mono Bold Italic font bytes. +pub const FONTS_LIBERATION_MONO_BOLD_ITALIC: &[u8] = + include_bytes!("assets/fonts/liberationmono/LiberationMono-BoldItalic-webfont.woff"); + +/// List of assets -- path and content. +pub const ASSETS: &[(&str, &[u8])] = &[ + ("webi/favicon.ico", PEACE_FAVICON_ICO), + ("webi/fonts/fonts.css", FONTS_LIBERATION_MONO_CSS_FONT_FACES), + ( + "webi/fonts/liberationmono/LiberationMono-Regular-webfont.woff", + FONTS_LIBERATION_MONO_REGULAR, + ), + ( + "webi/fonts/liberationmono/LiberationMono-Bold-webfont.woff", + FONTS_LIBERATION_MONO_BOLD, + ), + ( + "webi/fonts/liberationmono/LiberationMono-Italic-webfont.woff", + FONTS_LIBERATION_MONO_ITALIC, + ), + ( + "webi/fonts/liberationmono/LiberationMono-BoldItalic-webfont.woff", + FONTS_LIBERATION_MONO_BOLD_ITALIC, + ), +]; diff --git a/crate/webi_output/src/assets/favicon.ico b/crate/webi_output/src/assets/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c2dec900bcf00821ecb220f215d4bfa7261c16d8 GIT binary patch literal 15406 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 literal 0 HcmV?d00001 diff --git a/crate/webi_output/src/assets/fonts/fonts.css b/crate/webi_output/src/assets/fonts/fonts.css new file mode 100644 index 000000000..9891dc5b4 --- /dev/null +++ b/crate/webi_output/src/assets/fonts/fonts.css @@ -0,0 +1,33 @@ +/* + * Hand written, based on CSS from + * + * Paths are relative to this file. + */ + +@font-face { + font-family: 'liberationmono'; + src: url('liberationmono/LiberationMono-Regular-webfont.woff') format('woff'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'liberationmono-bold'; + src: url('liberationmono/LiberationMono-Bold-webfont.woff') format('woff'); + font-weight: bold; + font-style: normal; +} + +@font-face { + font-family: 'liberationmono-italic'; + src: url('liberationmono/LiberationMono-Italic-webfont.woff') format('woff'); + font-weight: normal; + font-style: italic; +} + +@font-face { + font-family: 'liberationmono-bold-italic'; + src: url('liberationmono/LiberationMono-BoldItalic-webfont.woff') format('woff'); + font-weight: normal; + font-style: italic; +} diff --git a/crate/webi_output/src/assets/fonts/liberationmono/LiberationMono-Bold-webfont.woff b/crate/webi_output/src/assets/fonts/liberationmono/LiberationMono-Bold-webfont.woff new file mode 100644 index 0000000000000000000000000000000000000000..75f6df9d2e6934e5e900735968223ed48a3028a5 GIT binary patch literal 20992 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 literal 0 HcmV?d00001 diff --git a/crate/webi_output/src/assets/fonts/liberationmono/LiberationMono-Regular-webfont.woff b/crate/webi_output/src/assets/fonts/liberationmono/LiberationMono-Regular-webfont.woff new file mode 100644 index 0000000000000000000000000000000000000000..965c9a341bbecf36021423a1e9a3f6252a118507 GIT binary patch literal 20764 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 }); + stream::iter(crate::assets::ASSETS.into_iter()) + .map(Result::<_, WebiError>::Ok) + .try_for_each(|(path_str, contents)| async move { + let asset_path = Path::new(path_str); + if let Some(parent_dir) = asset_path.parent() { + tokio::fs::create_dir_all(parent_dir) + .await + .map_err(|error| WebiError::AssetDirCreate { + asset_dir: parent_dir.to_path_buf(), + error, + })?; + } + + tokio::fs::write(asset_path, contents) + .await + .map_err(|error| WebiError::AssetWrite { + asset_path: asset_path.to_path_buf(), + error, + })?; + + Ok(()) + }) + .await?; + let router = Router::new() // serve the pkg directory .nest_service( "/pkg", - ServeDir::new(PathBuf::from(leptos_options.site_pkg_dir.as_str())), + ServeDir::new(Path::new(leptos_options.site_pkg_dir.as_str())), ) + // serve the `webi` directory + .nest_service("/webi", ServeDir::new(Path::new("webi"))) // serve the SSR rendered homepage .leptos_routes(&leptos_options, routes, move || view! { }) .with_state(leptos_options); diff --git a/examples/envman/Cargo.toml b/examples/envman/Cargo.toml index d639935be..f7b31ac4c 100644 --- a/examples/envman/Cargo.toml +++ b/examples/envman/Cargo.toml @@ -15,7 +15,7 @@ test = false [lib] doctest = false test = false -crate-type = ["cdylib"] +crate-type = ["cdylib", "rlib"] [dependencies] aws-config = { version = "1.1.4", optional = true } From 9c02837be2ec80b735308681e52ba56e0c3dcdee Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Sun, 11 Feb 2024 13:01:35 +1300 Subject: [PATCH 10/38] Remove `envman` web server and components. --- examples/envman/Cargo.toml | 11 +- examples/envman/src/lib.rs | 8 +- examples/envman/src/web.rs | 19 -- examples/envman/src/web/client.rs | 21 --- examples/envman/src/web/components.rs | 11 -- .../envman/src/web/components/flow_graph.rs | 93 ---------- examples/envman/src/web/components/home.rs | 25 --- examples/envman/src/web/flow_dot_renderer.rs | 169 ------------------ examples/envman/src/web/tailwind.config.js | 11 -- examples/envman/src/web/tailwind.css | 3 - examples/envman/src/web/web_server.rs | 56 ------ 11 files changed, 4 insertions(+), 423 deletions(-) delete mode 100644 examples/envman/src/web.rs delete mode 100644 examples/envman/src/web/client.rs delete mode 100644 examples/envman/src/web/components.rs delete mode 100644 examples/envman/src/web/components/flow_graph.rs delete mode 100644 examples/envman/src/web/components/home.rs delete mode 100644 examples/envman/src/web/flow_dot_renderer.rs delete mode 100644 examples/envman/src/web/tailwind.config.js delete mode 100644 examples/envman/src/web/tailwind.css delete mode 100644 examples/envman/src/web/web_server.rs diff --git a/examples/envman/Cargo.toml b/examples/envman/Cargo.toml index f7b31ac4c..711949385 100644 --- a/examples/envman/Cargo.toml +++ b/examples/envman/Cargo.toml @@ -90,7 +90,6 @@ cli = [ # enabled when we use `"web_server"` as the web server feature without enabling `"ssr"` as well. ssr = [ "flow_logic", - "web_components", "dep:axum", "dep:hyper", "dep:leptos_axum", @@ -104,7 +103,6 @@ ssr = [ "peace/ssr", ] csr = [ - "web_components", "peace/webi", ] @@ -138,16 +136,13 @@ flow_logic = [ ] # web related -web_components = [ - "dep:tracing", -] web_server = [ "ssr", # leptos generates functions that depend on this feature in the application crate. ] # leptos csr hydrate = [ - "web_components", + "dep:tracing", "leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate", @@ -173,8 +168,8 @@ site-pkg-dir = "pkg" # # Optional. Env: LEPTOS_STYLE_FILE. # style-file = "../../target/web/envman/public/css/tailwind.css" -tailwind-input-file = "src/web/tailwind.css" -tailwind-config-file = "src/web/tailwind.config.js" +# tailwind-input-file = "src/web/tailwind.css" +# tailwind-config-file = "src/web/tailwind.config.js" # Assets source dir. All files found here will be copied and synchronized to site-root. # The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir. diff --git a/examples/envman/src/lib.rs b/examples/envman/src/lib.rs index 99de5dcd9..598c0f016 100644 --- a/examples/envman/src/lib.rs +++ b/examples/envman/src/lib.rs @@ -59,13 +59,7 @@ cfg_if::cfg_if! { } cfg_if::cfg_if! { - if #[cfg(feature = "web_components")] { - pub mod web; - } -} - -cfg_if::cfg_if! { - if #[cfg(all(feature = "web_components", feature = "hydrate"))] { + if #[cfg(feature = "hydrate")] { use wasm_bindgen::prelude::wasm_bindgen; use leptos::*; diff --git a/examples/envman/src/web.rs b/examples/envman/src/web.rs deleted file mode 100644 index 4eba1a307..000000000 --- a/examples/envman/src/web.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! Runs `envman` as a web application. -//! -//! See for the leptos usage guide. - -pub use self::flow_dot_renderer::FlowDotRenderer; - -pub mod components; - -mod flow_dot_renderer; - -cfg_if::cfg_if! { - if #[cfg(feature = "ssr")] { - pub use self::web_server::WebServer; - - mod web_server; - } else if #[cfg(feature = "csr")] { - pub mod client; - } -} diff --git a/examples/envman/src/web/client.rs b/examples/envman/src/web/client.rs deleted file mode 100644 index 7140b8e28..000000000 --- a/examples/envman/src/web/client.rs +++ /dev/null @@ -1,21 +0,0 @@ -cfg_if::cfg_if! { - if #[cfg(feature = "hydrate")] { - use wasm_bindgen::prelude::wasm_bindgen; - use leptos::*; - - // use crate::{flows::AppUploadFlow, web::components::Home}; - - #[wasm_bindgen] - pub async fn hydrate() { - // initializes logging using the `log` crate - let _log = console_log::init_with_level(log::Level::Debug); - console_error_panic_hook::set_once(); - - leptos::mount_to_body(move || { - view! { -
rara
- } - }); - } - } -} diff --git a/examples/envman/src/web/components.rs b/examples/envman/src/web/components.rs deleted file mode 100644 index 12ed9455d..000000000 --- a/examples/envman/src/web/components.rs +++ /dev/null @@ -1,11 +0,0 @@ -#![allow(non_snake_case)] // Components are all PascalCase. - -//! Components for rendering a flow. - -pub use self::{flow_graph::FlowGraph, home::Home}; - -#[cfg(feature = "ssr")] -pub use self::flow_graph::FlowGraphSrc; - -mod flow_graph; -mod home; diff --git a/examples/envman/src/web/components/flow_graph.rs b/examples/envman/src/web/components/flow_graph.rs deleted file mode 100644 index 8268ac5df..000000000 --- a/examples/envman/src/web/components/flow_graph.rs +++ /dev/null @@ -1,93 +0,0 @@ -use leptos::{ - component, create_signal, server_fn::error::NoCustomError, view, IntoView, ServerFnError, - SignalGet, SignalUpdate, Transition, -}; - -/// Renders the flow graph. -#[component] -pub fn FlowGraph() -> impl IntoView { - let (count, set_count) = create_signal(0); - - let dot_source_resource = leptos::create_resource( - || (), - move |()| async move { flow_graph_src().await.unwrap() }, - ); - let dot_source_result = { - move || { - let dot_source = dot_source_resource - .get() - .unwrap_or_else(|| String::from("digraph {}")); - - let script_src = format!( - "\ - import {{ Graphviz }} from \"https://cdn.jsdelivr.net/npm/@hpcc-js/wasm/dist/graphviz.js\";\n\ - \n\ - const graphviz = await Graphviz.load();\n\ - const dot_source = `{dot_source}`;\n\ - document.getElementById(\"flow_dot_diagram\").innerHTML =\n\ - graphviz.layout(dot_source, \"svg\", \"dot\");\n\ - " - ); - - view! { - - } - } - }; - - view! { -
-
-
-
- "hello leptos!" { move || count.get() } -
- -
-
- "Loading graph..."

}> - { dot_source_result } -
- // Client side rendering, if we know what flow we have. - // - // This doesn't work for us because: - // - // * Loading a `flow` is `async`. - // * Component functions are `sync`. - // * Loading a resource using `leptos::create_resource` returns the loaded value between - // `` tags. - // * Such tags cannot be children of a ` - // ``` - } -} - -#[leptos::server(FlowGraphSrc, "/flow_graph")] -pub async fn flow_graph_src() -> Result> { - use crate::{flows::AppUploadFlow, web::FlowDotRenderer}; - let flow = AppUploadFlow::flow().await.map_err(|envman_error| { - ServerFnError::::ServerError(format!("{}", envman_error)) - })?; - - // use peace::rt_model::fn_graph::daggy::petgraph::dot::{Config, Dot}; - // let dot_source = Dot::with_config(cx.props.flow.graph().graph(), - // &[Config::EdgeNoLabel]); - - let flow_dot_renderer = FlowDotRenderer::new(); - Ok(flow_dot_renderer.dot(&flow)) -} diff --git a/examples/envman/src/web/components/home.rs b/examples/envman/src/web/components/home.rs deleted file mode 100644 index 5f2f195d1..000000000 --- a/examples/envman/src/web/components/home.rs +++ /dev/null @@ -1,25 +0,0 @@ -use leptos::{component, view, IntoView}; -use leptos_meta::{provide_meta_context, Link, Stylesheet}; -use leptos_router::{Route, Router, Routes}; - -use crate::web::components::FlowGraph; - -#[component] -pub fn Home() -> impl IntoView { - // Provides context that manages stylesheets, titles, meta tags, etc. - provide_meta_context(); - - view! { - - - -
- - - }/> - -
-
- } -} diff --git a/examples/envman/src/web/flow_dot_renderer.rs b/examples/envman/src/web/flow_dot_renderer.rs deleted file mode 100644 index 6e746f0c8..000000000 --- a/examples/envman/src/web/flow_dot_renderer.rs +++ /dev/null @@ -1,169 +0,0 @@ -use peace::{cfg::ItemId, rt_model::Flow}; - -/// Renders a `Flow` as a GraphViz Dot diagram. -/// -/// This is currently a mashed together implementation. A proper implementation -/// would require investigation into: -/// -/// * Whether colours and border sizes should be part of a theme object that we -/// take in. -/// * Whether colours and border sizes can be configured through CSS classes, -/// and within the generated dot source, we only apply those classes. -#[derive(Debug)] -pub struct FlowDotRenderer { - edge_color: &'static str, - node_text_color: &'static str, - plain_text_color: &'static str, -} - -impl FlowDotRenderer { - pub fn new() -> Self { - Self { - edge_color: "#7f7f7f", - node_text_color: "#111111", - plain_text_color: "#7f7f7f", - } - } - - pub fn dot(&self, flow: &Flow) -> String - where - E: 'static, - { - let graph_attrs = self.graph_attrs(); - let node_attrs = self.node_attrs(); - let edge_attrs = self.edge_attrs(); - - let item_clusters = flow - .graph() - .iter() - .map(|item| self.item_cluster(item.id())) - .collect::>() - .join("\n"); - - let edges = flow - .graph() - .raw_edges() - .iter() - .map(|edge| { - let src_item = &flow.graph()[edge.source()]; - let src_item_id = src_item.id(); - let target_item = &flow.graph()[edge.target()]; - let target_item_id = target_item.id(); - self.edge(src_item_id, target_item_id) - }) - .collect::>() - .join("\n"); - - format!( - "digraph G {{ - {graph_attrs} - {node_attrs} - {edge_attrs} - - {item_clusters} - - {edges} - }}" - ) - } - - fn graph_attrs(&self) -> String { - let plain_text_color = self.plain_text_color; - // Note: `margin` is set to 0.1 because some text lies outside the viewport. - // This may be due to incorrect width calculation for emoji characters, which - // GraphViz falls back to the space character width. - format!( - "\ - graph [\n\ - margin = 0.1\n\ - penwidth = 0\n\ - nodesep = 0.0\n\ - ranksep = 0.02\n\ - bgcolor = \"transparent\"\n\ - fontname = \"helvetica\"\n\ - fontcolor = \"{plain_text_color}\"\n\ - splines = line\n\ - rankdir = LR\n\ - ]\n\ - " - ) - } - - fn node_attrs(&self) -> String { - let node_text_color = self.node_text_color; - format!( - "\ - node [\n\ - fontcolor = \"{node_text_color}\"\n\ - fontname = \"monospace\"\n\ - fontsize = 12\n\ - shape = \"circle\"\n\ - style = \"filled\"\n\ - width = 0.3\n\ - height = 0.3\n\ - margin = 0.04\n\ - color = \"#9999aa\"\n\ - fillcolor = \"#ddddf5\"\n\ - ]\n\ - " - ) - } - - fn edge_attrs(&self) -> String { - let edge_color = self.edge_color; - let plain_text_color = self.plain_text_color; - format!( - "\ - edge [\n\ - arrowsize = 0.7\n\ - color = \"{edge_color}\"\n\ - fontcolor = \"{plain_text_color}\"\n\ - ]\n\ - " - ) - } - - fn item_cluster(&self, item_id: &ItemId) -> String { - let plain_text_color = self.plain_text_color; - let classes = "\ - [&>ellipse]:fill-slate-300 \ - [&>ellipse]:stroke-1 \ - [&>ellipse]:stroke-slate-600 \ - [&>ellipse]:hover:fill-slate-200 \ - [&>ellipse]:hover:stroke-slate-600 \ - [&>ellipse]:hover:stroke-2 \ - cursor-pointer \ - "; - format!( - r#" - subgraph cluster_{item_id} {{ - {item_id} [label = "" class = "{classes}"] - {item_id}_text [ - shape="plain" - style="" - fontcolor="{plain_text_color}" - label = < - - - - -
📥{item_id}
> - ] - }} - "# - ) - } - - fn edge(&self, src_item_id: &ItemId, target_item_id: &ItemId) -> String { - format!(r#"{src_item_id} -> {target_item_id} [minlen = 9]"#) - } -} - -impl Default for FlowDotRenderer { - fn default() -> Self { - Self::new() - } -} diff --git a/examples/envman/src/web/tailwind.config.js b/examples/envman/src/web/tailwind.config.js deleted file mode 100644 index 892ec9cf4..000000000 --- a/examples/envman/src/web/tailwind.config.js +++ /dev/null @@ -1,11 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -module.exports = { - content: [ - // Relative to workspace root. - 'examples/envman/**/*.rs', - ], - theme: { - extend: {}, - }, - plugins: [], -} diff --git a/examples/envman/src/web/tailwind.css b/examples/envman/src/web/tailwind.css deleted file mode 100644 index b5c61c956..000000000 --- a/examples/envman/src/web/tailwind.css +++ /dev/null @@ -1,3 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; diff --git a/examples/envman/src/web/web_server.rs b/examples/envman/src/web/web_server.rs deleted file mode 100644 index f6da73818..000000000 --- a/examples/envman/src/web/web_server.rs +++ /dev/null @@ -1,56 +0,0 @@ -use std::{net::SocketAddr, path::PathBuf}; - -use axum::Router; -use leptos::view; -use leptos_axum::LeptosRoutes; -use tokio::io::AsyncWriteExt; -use tower_http::services::ServeDir; - -use crate::{model::EnvManError, web::components::Home}; - -/// Web server that responds to `envman` requests. -#[derive(Debug)] -pub struct WebServer {} - -impl WebServer { - /// Starts the web server. - pub async fn start(socket_addr: Option) -> Result<(), EnvManError> { - // Setting this to None means we'll be using cargo-leptos and its env vars - let conf = leptos::get_configuration(None).await.unwrap(); - let leptos_options = conf.leptos_options; - let socket_addr = socket_addr.unwrap_or(leptos_options.site_addr); - let routes = leptos_axum::generate_route_list(|| view! { }); - - let router = Router::new() - // serve the pkg directory - .nest_service( - "/pkg", - ServeDir::new(PathBuf::from_iter([ - leptos_options.site_root.as_str(), - leptos_options.site_pkg_dir.as_str(), - ])), - ) - // serve the SSR rendered homepage - .leptos_routes(&leptos_options, routes, move || view! { }) - .with_state(leptos_options); - - let listener = tokio::net::TcpListener::bind(socket_addr) - .await - .unwrap_or_else(|e| panic!("Failed to listen on {socket_addr}. Error: {e}")); - let (Ok(()) | Err(_)) = tokio::io::stderr() - .write_all(format!("listening on http://{}\n", socket_addr).as_bytes()) - .await; - let (Ok(()) | Err(_)) = tokio::io::stderr() - .write_all( - format!( - "working dir: {}\n", - std::env::current_dir().unwrap().display() - ) - .as_bytes(), - ) - .await; - axum::serve(listener, router) - .await - .map_err(|error| EnvManError::WebServerServe { error }) - } -} From 4ef79f479564204b711a4ebb335c40ae64d3f034 Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Thu, 15 Feb 2024 18:47:09 +1300 Subject: [PATCH 11/38] Collapse `T: Params + ..` additional bounds into `Params` supertraits. --- crate/params/src/params.rs | 2 +- crate/params/src/params_spec.rs | 10 +++++----- crate/params/src/params_spec_de.rs | 5 +++-- .../item/item_parameters/params_specification.md | 2 +- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/crate/params/src/params.rs b/crate/params/src/params.rs index 7a7d76e68..c00c8451a 100644 --- a/crate/params/src/params.rs +++ b/crate/params/src/params.rs @@ -7,7 +7,7 @@ use crate::FieldWiseSpecRt; /// Input parameters to an item. /// /// This trait is automatically implemented by `#[derive(Value)]`. -pub trait Params { +pub trait Params: Clone + Debug + Serialize + DeserializeOwned + Send + Sync + 'static { /// Convenience associated type for `ValueSpec`. type Spec: Clone + Debug + Serialize + DeserializeOwned + Send + Sync + 'static; /// The `Value` type, but with optional fields. diff --git a/crate/params/src/params_spec.rs b/crate/params/src/params_spec.rs index 583112806..f8a9fdcd8 100644 --- a/crate/params/src/params_spec.rs +++ b/crate/params/src/params_spec.rs @@ -28,10 +28,10 @@ use crate::{ /// 4. These `AnySpecRtBoxed`s are downcasted back to `ValueSpec` when /// resolving values for item params and params partials. #[derive(Clone, Serialize, Deserialize)] -#[serde(from = "crate::ParamsSpecDe")] +#[serde(from = "crate::ParamsSpecDe", bound = "T: Params")] pub enum ParamsSpec where - T: Params + Clone + Debug + Send + Sync + 'static, + T: Params, { /// Loads a stored value spec. /// @@ -95,7 +95,7 @@ where impl ParamsSpec where - T: Params + Clone + Debug + Send + Sync + 'static, + T: Params, { pub fn from_map(field_name: Option, f: F) -> Self where @@ -108,7 +108,7 @@ where impl Debug for ParamsSpec where - T: Params + Clone + Debug + Send + Sync + 'static, + T: Params, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -125,7 +125,7 @@ where impl From for ParamsSpec where - T: Params + Clone + Debug + Send + Sync + 'static, + T: Params, { fn from(value: T) -> Self { Self::Value { value } diff --git a/crate/params/src/params_spec_de.rs b/crate/params/src/params_spec_de.rs index 8ef29e498..0f370e6df 100644 --- a/crate/params/src/params_spec_de.rs +++ b/crate/params/src/params_spec_de.rs @@ -8,6 +8,7 @@ type FnPlaceholder = fn(&()) -> Option; /// Exists to deserialize `MappingFn` with a non-type-erased `MappingFnImpl` #[derive(Clone, Deserialize)] +#[serde(bound = "T: Params")] pub enum ParamsSpecDe where T: Params, @@ -53,7 +54,7 @@ where impl Debug for ParamsSpecDe where - T: Params + Debug, + T: Params, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -72,7 +73,7 @@ where impl From> for ParamsSpec where - T: Params + Clone + Debug + Send + Sync + 'static, + T: Params, { fn from(value_spec_de: ParamsSpecDe) -> Self { match value_spec_de { diff --git a/doc/src/technical_concepts/item/item_parameters/params_specification.md b/doc/src/technical_concepts/item/item_parameters/params_specification.md index 09236fac0..f4f6b7610 100644 --- a/doc/src/technical_concepts/item/item_parameters/params_specification.md +++ b/doc/src/technical_concepts/item/item_parameters/params_specification.md @@ -99,7 +99,7 @@ The following snippets are here to show the changes that include the above conce ```rust ,ignore // Traits in Peace Framework trait Item { - type Params: Params + Serialize + Deserialize; + type Params: Params; fn setup(&self, resources); fn try_state_current(fn_ctx, params_partial, data); From ae747377a2e00fc2079c6d7007c43048224047c5 Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Sat, 17 Feb 2024 12:24:06 +1300 Subject: [PATCH 12/38] Add `peace_flow_model` with initial data to transfer. --- Cargo.toml | 6 ++++-- crate/core/src/item_id.rs | 4 +++- crate/flow_model/Cargo.toml | 19 +++++++++++++++++++ crate/flow_model/src/flow_info.rs | 19 +++++++++++++++++++ crate/flow_model/src/flow_spec_info.rs | 17 +++++++++++++++++ crate/flow_model/src/item_info.rs | 11 +++++++++++ crate/flow_model/src/item_spec_info.rs | 11 +++++++++++ crate/flow_model/src/lib.rs | 14 ++++++++++++++ src/lib.rs | 1 + 9 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 crate/flow_model/Cargo.toml create mode 100644 crate/flow_model/src/flow_info.rs create mode 100644 crate/flow_model/src/flow_spec_info.rs create mode 100644 crate/flow_model/src/item_info.rs create mode 100644 crate/flow_model/src/item_spec_info.rs create mode 100644 crate/flow_model/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index f6f01a246..e630e797f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ peace_cmd_model = { workspace = true } peace_cmd_rt = { workspace = true } peace_data = { workspace = true } peace_diff = { workspace = true } +peace_flow_model = { workspace = true } peace_fmt = { workspace = true } peace_params = { workspace = true } peace_resources = { workspace = true } @@ -105,6 +106,7 @@ peace_core = { path = "crate/core", version = "0.0.13" } peace_data = { path = "crate/data", version = "0.0.13" } peace_data_derive = { path = "crate/data_derive", version = "0.0.13" } peace_diff = { path = "crate/diff", version = "0.0.13" } +peace_flow_model = { path = "crate/flow_model", version = "0.0.13" } peace_fmt = { path = "crate/fmt", version = "0.0.13" } peace_params = { path = "crate/params", version = "0.0.13" } peace_params_derive = { path = "crate/params_derive", version = "0.0.13" } @@ -173,10 +175,10 @@ syn = "2.0.48" tar = "0.4.40" tempfile = "3.9.0" thiserror = "1.0.56" -tokio = "1.35.1" +tokio = "1.36" tokio-util = "0.7.10" tower-http = "0.5.1" -tynm = "0.1.9" +tynm = "0.1.10" type_reg = { version = "0.7.0", features = ["debug", "untagged", "ordered"] } url = "2.5.0" wasm-bindgen = "0.2.90" diff --git a/crate/core/src/item_id.rs b/crate/core/src/item_id.rs index 43d2c7466..5d7ed01f2 100644 --- a/crate/core/src/item_id.rs +++ b/crate/core/src/item_id.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use serde::{Deserialize, Serialize}; -/// Unique identifier for an `ItemId`, `Cow<'static, str>` newtype. +/// Unique identifier for an [`Item`], `Cow<'static, str>` newtype. /// /// Must begin with a letter or underscore, and contain only letters, numbers, /// and underscores. @@ -30,6 +30,8 @@ use serde::{Deserialize, Serialize}; /// * Read state using the old ID. /// * Either clean up that state, or migrate that state into an Item with the /// new ID. +/// +/// [`Item`]: https://docs.rs/peace_cfg/latest/peace_cfg/trait.Item.html #[derive(Clone, Debug, Hash, PartialEq, Eq, Deserialize, Serialize)] pub struct ItemId(Cow<'static, str>); diff --git a/crate/flow_model/Cargo.toml b/crate/flow_model/Cargo.toml new file mode 100644 index 000000000..f5e3b0707 --- /dev/null +++ b/crate/flow_model/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "peace_flow_model" +description = "Flow data model for the peace automation framework." +documentation = "https://docs.rs/peace_flow_model/" +version.workspace = true +authors.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true +readme.workspace = true +categories.workspace = true +keywords.workspace = true +license.workspace = true + +[dependencies] +fn_graph = { workspace = true, features = ["graph_info"] } +peace_core = { workspace = true } +serde = { workspace = true, features = ["derive"] } +tynm = { workspace = true, features = ["info", "serde"] } diff --git a/crate/flow_model/src/flow_info.rs b/crate/flow_model/src/flow_info.rs new file mode 100644 index 000000000..fee60679b --- /dev/null +++ b/crate/flow_model/src/flow_info.rs @@ -0,0 +1,19 @@ +use fn_graph::GraphInfo; +use peace_core::FlowId; + +use serde::{Deserialize, Serialize}; + +use crate::ItemInfo; + +/// Serializable representation of values in a [`Flow`]. +/// +/// This includes values passed into, or produced by `Item`s in the `Flow`. +/// +/// [`Flow`]: https://docs.rs/peace_rt_model/latest/peace_rt_model/struct.Flow.html +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct FlowInfo { + /// ID of the flow. + pub flow_id: FlowId, + /// Serialized representation of the flow graph. + pub graph_info: GraphInfo, +} diff --git a/crate/flow_model/src/flow_spec_info.rs b/crate/flow_model/src/flow_spec_info.rs new file mode 100644 index 000000000..f73eaaf01 --- /dev/null +++ b/crate/flow_model/src/flow_spec_info.rs @@ -0,0 +1,17 @@ +use fn_graph::GraphInfo; +use peace_core::FlowId; + +use serde::{Deserialize, Serialize}; + +use crate::ItemSpecInfo; + +/// Serializable representation of how a [`Flow`] is configured. +/// +/// [`Flow`]: https://docs.rs/peace_rt_model/latest/peace_rt_model/struct.Flow.html +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct FlowSpecInfo { + /// ID of the flow. + pub flow_id: FlowId, + /// Serialized representation of the flow graph. + pub graph_info: GraphInfo, +} diff --git a/crate/flow_model/src/item_info.rs b/crate/flow_model/src/item_info.rs new file mode 100644 index 000000000..6b1656615 --- /dev/null +++ b/crate/flow_model/src/item_info.rs @@ -0,0 +1,11 @@ +use peace_core::ItemId; +use serde::{Deserialize, Serialize}; + +/// Serializable representation of values used for / produced by an [`Item`]. +/// +/// [`Item`]: https://docs.rs/peace_cfg/latest/peace_cfg/trait.Item.html +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct ItemInfo { + /// ID of the `Item`. + pub item_id: ItemId, +} diff --git a/crate/flow_model/src/item_spec_info.rs b/crate/flow_model/src/item_spec_info.rs new file mode 100644 index 000000000..a95f948f0 --- /dev/null +++ b/crate/flow_model/src/item_spec_info.rs @@ -0,0 +1,11 @@ +use peace_core::ItemId; +use serde::{Deserialize, Serialize}; + +/// Serializable representation of how an [`Item`] is configured. +/// +/// [`Item`]: https://docs.rs/peace_cfg/latest/peace_cfg/trait.Item.html +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct ItemSpecInfo { + /// ID of the `Item`. + pub item_id: ItemId, +} diff --git a/crate/flow_model/src/lib.rs b/crate/flow_model/src/lib.rs new file mode 100644 index 000000000..e8822b35f --- /dev/null +++ b/crate/flow_model/src/lib.rs @@ -0,0 +1,14 @@ +//! Flow data model for the peace automation framework. +//! +//! This includes the serializable representation of a `Flow`. Since an actual +//! `Flow` contains logic, it currently resides in `peace_rt_model`. + +pub use crate::{ + flow_info::FlowInfo, flow_spec_info::FlowSpecInfo, item_info::ItemInfo, + item_spec_info::ItemSpecInfo, +}; + +mod flow_info; +mod flow_spec_info; +mod item_info; +mod item_spec_info; diff --git a/src/lib.rs b/src/lib.rs index 566ae7ed4..45eb7fef5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,7 @@ pub use peace_cmd_model as cmd_model; pub use peace_cmd_rt as cmd_rt; pub use peace_data as data; pub use peace_diff as diff; +pub use peace_flow_model as flow_model; pub use peace_fmt as fmt; pub use peace_params as params; pub use peace_resources as resources; From 2cb581bf05f09dda52685af4ccbf3f17b6c3f917 Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Sat, 17 Feb 2024 20:57:45 +1300 Subject: [PATCH 13/38] First cut of getting `WebiOutput` to serve `FlowSpecGraph`. --- Cargo.toml | 4 + crate/cmd/src/ctx/cmd_ctx_builder.rs | 7 +- crate/flow_model/Cargo.toml | 1 + crate/flow_model/src/flow_spec_info.rs | 223 +++++++++++++++++++++++- crate/rt_model/Cargo.toml | 1 + crate/rt_model/src/flow.rs | 19 ++ crate/webi_components/Cargo.toml | 3 + crate/webi_components/src/flow_graph.rs | 51 +++--- crate/webi_output/Cargo.toml | 1 + crate/webi_output/src/webi_output.rs | 25 ++- doc/src/ideas.md | 54 ++++++ examples/envman/src/main.rs | 11 +- examples/envman/src/main_cli.rs | 6 +- 13 files changed, 366 insertions(+), 40 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e630e797f..da5c345a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -146,6 +146,7 @@ console = "0.15.8" derivative = "2.2.0" diff-struct = "0.5.3" downcast-rs = "1.2.0" +dot_ix = { path = "/mnt/data/work/github/azriel91/dot_ix" } dyn-clone = "1.0.16" enser = "0.1.4" erased-serde = "0.4.2" @@ -183,3 +184,6 @@ type_reg = { version = "0.7.0", features = ["debug", "untagged", "ordered"] } url = "2.5.0" wasm-bindgen = "0.2.90" web-sys = "0.3.67" + +[patch.crates-io] +fn_graph = { path = "/mnt/data/work/github/azriel91/fn_graph" } diff --git a/crate/cmd/src/ctx/cmd_ctx_builder.rs b/crate/cmd/src/ctx/cmd_ctx_builder.rs index 8ceef338e..e2c1c8608 100644 --- a/crate/cmd/src/ctx/cmd_ctx_builder.rs +++ b/crate/cmd/src/ctx/cmd_ctx_builder.rs @@ -300,8 +300,8 @@ where // Removing the entry from stored params specs is deliberate, so filtering for // stored params specs that no longer have a corresponding item are // detected. - let params_spec_provided = params_specs_provided.remove_entry(item_id); - let params_spec_stored = params_specs_stored.remove_entry(item_id); + let params_spec_provided = params_specs_provided.shift_remove_entry(item_id); + let params_spec_stored = params_specs_stored.shift_remove_entry(item_id); // Deep merge params specs. let params_spec_to_use = match (params_spec_provided, params_spec_stored) { @@ -344,7 +344,8 @@ where item_graph.iter_insertion().for_each(|item_rt| { let item_id = item_rt.id(); - if let Some((item_id, params_spec_boxed)) = params_specs_provided.remove_entry(item_id) + if let Some((item_id, params_spec_boxed)) = + params_specs_provided.shift_remove_entry(item_id) { params_specs.insert_raw(item_id, params_spec_boxed); } else { diff --git a/crate/flow_model/Cargo.toml b/crate/flow_model/Cargo.toml index f5e3b0707..045d888c0 100644 --- a/crate/flow_model/Cargo.toml +++ b/crate/flow_model/Cargo.toml @@ -13,6 +13,7 @@ keywords.workspace = true license.workspace = true [dependencies] +dot_ix = { workspace = true } fn_graph = { workspace = true, features = ["graph_info"] } peace_core = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/crate/flow_model/src/flow_spec_info.rs b/crate/flow_model/src/flow_spec_info.rs index f73eaaf01..445b603c6 100644 --- a/crate/flow_model/src/flow_spec_info.rs +++ b/crate/flow_model/src/flow_spec_info.rs @@ -1,4 +1,10 @@ -use fn_graph::GraphInfo; +use std::collections::HashSet; + +use dot_ix::model::{ + common::{EdgeId, NodeHierarchy, NodeId}, + info_graph::{GraphDir, IndexMap, InfoGraph, NodeInfo}, +}; +use fn_graph::{daggy::Walker, Edge, FnId, GraphInfo}; use peace_core::FlowId; use serde::{Deserialize, Serialize}; @@ -15,3 +21,218 @@ pub struct FlowSpecInfo { /// Serialized representation of the flow graph. pub graph_info: GraphInfo, } + +impl FlowSpecInfo { + /// Returns an [`InfoGraph`] that represents the progress of the flow's + /// execution. + pub fn into_progress_info_graph(&self) -> InfoGraph { + let graph_info = &self.graph_info; + let item_count = graph_info.node_count(); + + let hierarchy = graph_info.iter_insertion_with_indices().fold( + NodeHierarchy::with_capacity(item_count), + |mut hierarchy, (_node_index, item_spec_info)| { + let node_id = item_spec_info_to_node_id(item_spec_info); + // Progress nodes have no nested nodes. + hierarchy.insert(node_id, NodeHierarchy::new()); + hierarchy + }, + ); + + let edges = progress_node_edges(graph_info); + let node_infos = node_infos(graph_info); + + InfoGraph::builder() + .with_direction(GraphDir::Vertical) + .with_hierarchy(hierarchy) + .with_edges(edges) + .with_node_infos(node_infos) + .build() + } + + /// Returns an [`InfoGraph`] that represents the outcome of the flow's + /// execution. + pub fn into_outcome_info_graph(&self) -> InfoGraph { + let graph_info = &self.graph_info; + let item_count = graph_info.node_count(); + + let mut visited = HashSet::with_capacity(item_count); + let visited = &mut visited; + let hierarchy = graph_info + .iter_insertion_with_indices() + .filter_map(|(node_index, item_spec_info)| { + let node_hierarchy = outcome_node_hierarchy(graph_info, visited, node_index); + let node_id = item_spec_info_to_node_id(item_spec_info); + node_hierarchy.map(|node_hierarchy| (node_id, node_hierarchy)) + }) + .fold( + NodeHierarchy::new(), + |mut hierarchy, (node_id, node_hierarchy)| { + hierarchy.insert(node_id, node_hierarchy); + hierarchy + }, + ); + + let edges = outcome_node_edges(graph_info); + let node_infos = node_infos(graph_info); + + InfoGraph::builder() + .with_direction(GraphDir::Vertical) + .with_hierarchy(hierarchy) + .with_edges(edges) + .with_node_infos(node_infos) + .build() + } +} + +/// Returns a `NodeHierarchy` for the given node, if it has not already been +/// visited. +fn outcome_node_hierarchy( + graph_info: &GraphInfo, + visited: &mut HashSet, + node_index: FnId, +) -> Option { + if visited.contains(&node_index) { + return None; + } + visited.insert(node_index); + + let mut hierarchy = NodeHierarchy::new(); + let children = graph_info.children(node_index); + children + .iter(&*graph_info) + .filter_map(|(edge_index, child_node_index)| { + // For outcome graphs, child nodes that: + // + // * are contained by parents nodes are represented as a nested node. + // * reference data from parent nodes are represented by forward edges + if matches!( + graph_info.edge_weight(edge_index).copied(), + Some(Edge::Contains) + ) { + Some(child_node_index) + } else { + None + } + }) + .for_each(|child_node_index| { + if let Some(child_node_hierarchy) = + outcome_node_hierarchy(graph_info, visited, child_node_index) + { + let item_spec_info = graph_info.node_weight(node_index).unwrap_or_else(|| { + panic!("`node_index`: `{node_index:?}` is invalid when accessing `flow_item_spec.graph_info`") + }); + hierarchy.insert( + item_spec_info_to_node_id(item_spec_info), + child_node_hierarchy, + ); + } + }); + + Some(hierarchy) +} + +/// Returns the list of edges between items in the graph. +fn outcome_node_edges(graph_info: &GraphInfo) -> IndexMap { + graph_info.iter_insertion_with_indices().fold( + IndexMap::with_capacity(graph_info.node_count()), + |mut edges, (node_index, item_spec_info)| { + // + let children = graph_info.children(node_index); + children + .iter(&*graph_info) + .filter_map(|(edge_index, child_node_index)| { + // For outcome graphs, child nodes that: + // + // * are contained by parents nodes are represented as a nested node. + // * reference data from parent nodes are represented by forward edges + if matches!( + graph_info.edge_weight(edge_index).copied(), + Some(Edge::Logic) + ) { + Some(child_node_index) + } else { + None + } + }) + .for_each(|child_node_index| { + let item_id = item_spec_info_to_node_id(item_spec_info); + let child_item_id = item_spec_info_to_node_id(&graph_info[child_node_index]); + edges.insert( + EdgeId::try_from(format!("{child_item_id}__{item_id}")).expect( + "Expected `peace` `ItemId`s concatenated \ + to be valid `dot_ix` `EdgeId`s.", + ), + [item_id, child_item_id], + ); + }); + + edges + }, + ) +} + +/// Returns the list of edges between items in the graph for progress. +/// +/// For progress graphs, an edge is rendered between pairs of predecessor and +/// successor items, regardless of whether their dependency is `Edge::Logic` +/// (adjacent) or `Edge::Contains` (nested). +fn progress_node_edges(graph_info: &GraphInfo) -> IndexMap { + graph_info.iter_insertion_with_indices().fold( + IndexMap::with_capacity(graph_info.node_count()), + |mut edges, (node_index, item_spec_info)| { + // + let children = graph_info.children(node_index); + children + .iter(&*graph_info) + .filter_map(|(edge_index, child_node_index)| { + // + // * are contained by parents nodes are represented as a nested node. + // * reference data from parent nodes are represented by forward edges + if matches!( + graph_info.edge_weight(edge_index).copied(), + Some(Edge::Logic | Edge::Contains) + ) { + Some(child_node_index) + } else { + None + } + }) + .for_each(|child_node_index| { + let item_id = item_spec_info_to_node_id(item_spec_info); + let child_item_id = item_spec_info_to_node_id(&graph_info[child_node_index]); + edges.insert( + EdgeId::try_from(format!("{child_item_id}__{item_id}")).expect( + "Expected `peace` `ItemId`s concatenated \ + to be valid `dot_ix` `EdgeId`s.", + ), + [item_id, child_item_id], + ); + }); + + edges + }, + ) +} + +/// Returns the list of edges between items in the graph. +fn node_infos(graph_info: &GraphInfo) -> IndexMap { + graph_info.iter_insertion_with_indices().fold( + IndexMap::with_capacity(graph_info.node_count()), + |mut node_infos, (_node_index, item_spec_info)| { + let item_id = item_spec_info_to_node_id(item_spec_info); + + // Note: This does not have to be the ID, it can be a human readable name. + let node_info = NodeInfo::new(item_id.to_string()); + + node_infos.insert(item_id, node_info); + + node_infos + }, + ) +} + +fn item_spec_info_to_node_id(item_spec_info: &ItemSpecInfo) -> NodeId { + NodeId::try_from(item_spec_info.item_id.to_string()) + .expect("Expected `peace` `ItemId`s to be valid `dot_ix` `NodeId`s.`") +} diff --git a/crate/rt_model/Cargo.toml b/crate/rt_model/Cargo.toml index 1d97a696d..20a4223f3 100644 --- a/crate/rt_model/Cargo.toml +++ b/crate/rt_model/Cargo.toml @@ -25,6 +25,7 @@ indicatif = { workspace = true, features = ["tokio"] } miette = { workspace = true, optional = true } peace_cfg = { workspace = true } peace_data = { workspace = true } +peace_flow_model = { workspace = true } peace_fmt = { workspace = true } peace_params = { workspace = true } peace_resources = { workspace = true } diff --git a/crate/rt_model/src/flow.rs b/crate/rt_model/src/flow.rs index 33a8b109d..2780f7258 100644 --- a/crate/rt_model/src/flow.rs +++ b/crate/rt_model/src/flow.rs @@ -1,4 +1,6 @@ use peace_cfg::FlowId; +use peace_data::fn_graph::GraphInfo; +use peace_flow_model::{FlowSpecInfo, ItemSpecInfo}; use crate::ItemGraph; @@ -58,4 +60,21 @@ impl Flow { pub fn graph_mut(&self) -> &ItemGraph { &self.graph } + + /// Generates a `FlowSpecInfo` from this `Flow`'s information. + pub fn flow_spec_info(&self) -> FlowSpecInfo + where + E: 'static, + { + let flow_id = self.flow_id.clone(); + let graph_info = GraphInfo::from_graph(&self.graph, |item_boxed| { + let item_id = item_boxed.id().clone(); + ItemSpecInfo { item_id } + }); + + FlowSpecInfo { + flow_id, + graph_info, + } + } } diff --git a/crate/webi_components/Cargo.toml b/crate/webi_components/Cargo.toml index 83e4cb2a1..d06da6c25 100644 --- a/crate/webi_components/Cargo.toml +++ b/crate/webi_components/Cargo.toml @@ -17,13 +17,16 @@ doctest = true test = false [dependencies] +dot_ix = { workspace = true } leptos = { workspace = true } leptos_meta = { workspace = true } leptos_router = { workspace = true } +peace_flow_model = { workspace = true } [features] default = [] ssr = [ + "dot_ix/ssr", "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", diff --git a/crate/webi_components/src/flow_graph.rs b/crate/webi_components/src/flow_graph.rs index 373a487a5..adabb9174 100644 --- a/crate/webi_components/src/flow_graph.rs +++ b/crate/webi_components/src/flow_graph.rs @@ -1,3 +1,4 @@ +use dot_ix::{model::common::DotSrcAndStyles, web_components::DotSvg}; use leptos::{ component, create_signal, server_fn::error::NoCustomError, view, IntoView, ServerFnError, SignalGet, SignalUpdate, Transition, @@ -10,35 +11,20 @@ pub fn FlowGraph() -> impl IntoView { let dot_source_resource = leptos::create_resource( || (), - move |()| async move { flow_graph_src().await.unwrap() }, + move |()| async move { progress_dot_graph().await.unwrap() }, ); - let dot_source_result = { - move || { - let dot_source = dot_source_resource - .get() - .unwrap_or_else(|| String::from("digraph { a -> b; }")); - - let script_src = format!( - "\ - import {{ Graphviz }} from \"https://cdn.jsdelivr.net/npm/@hpcc-js/wasm/dist/graphviz.js\";\n\ - \n\ - const graphviz = await Graphviz.load();\n\ - const dot_source = `{dot_source}`;\n\ - document.getElementById(\"flow_dot_diagram\").innerHTML =\n\ - graphviz.layout(dot_source, \"svg\", \"dot\");\n\ - " - ); - - view! { - - } - } + + let progress_dot_graph = move || { + let progress_dot_graph = dot_source_resource + .get() + .expect("Expected `progress_dot_graph` to always be generated successfully."); + + Some(progress_dot_graph) }; view! {
+

@@ -50,12 +36,23 @@ pub fn FlowGraph() -> impl IntoView {
"Loading graph..."

}> - { dot_source_result } +
} } #[leptos::server(endpoint = "/flow_graph")] -pub async fn flow_graph_src() -> Result> { - Ok(String::from("digraph { a; b; c; a -> c; b -> c; }")) +pub async fn progress_dot_graph() -> Result> { + use dot_ix::{model::common::GraphvizDotTheme, rt::IntoGraphvizDotSrc}; + use peace_flow_model::FlowSpecInfo; + + let flow_spec_info = leptos::use_context::().ok_or_else(|| { + ServerFnError::::ServerError("`FlowSpecInfo` was not set.".to_string()) + })?; + + let progress_info_graph = flow_spec_info.into_progress_info_graph(); + Ok(IntoGraphvizDotSrc::into( + &progress_info_graph, + &GraphvizDotTheme::default(), + )) } diff --git a/crate/webi_output/Cargo.toml b/crate/webi_output/Cargo.toml index 1a71369ff..1823151f2 100644 --- a/crate/webi_output/Cargo.toml +++ b/crate/webi_output/Cargo.toml @@ -25,6 +25,7 @@ leptos_axum = { workspace = true } leptos_meta = { workspace = true } leptos_router = { workspace = true } peace_core = { workspace = true, optional = true } +peace_flow_model = { workspace = true } peace_fmt = { workspace = true } peace_rt_model_core = { workspace = true } peace_value_traits = { workspace = true } diff --git a/crate/webi_output/src/webi_output.rs b/crate/webi_output/src/webi_output.rs index ebb41d371..e35b9c98e 100644 --- a/crate/webi_output/src/webi_output.rs +++ b/crate/webi_output/src/webi_output.rs @@ -4,6 +4,7 @@ use axum::Router; use futures::stream::{self, StreamExt, TryStreamExt}; use leptos::view; use leptos_axum::LeptosRoutes; +use peace_flow_model::FlowSpecInfo; use peace_fmt::Presentable; use peace_rt_model_core::{async_trait, output::OutputWrite}; use peace_value_traits::AppError; @@ -31,23 +32,31 @@ cfg_if::cfg_if! { pub struct WebiOutput { /// IP address and port to listen on. socket_addr: Option, + /// Flow to display to the user. + flow_spec_info: FlowSpecInfo, } impl WebiOutput { - pub fn new(socket_addr: Option) -> Self { - Self { socket_addr } + pub fn new(socket_addr: Option, flow_spec_info: FlowSpecInfo) -> Self { + Self { + socket_addr, + flow_spec_info, + } } } impl WebiOutput { pub async fn start(&self) -> Result<(), WebiError> { - let Self { socket_addr } = self; + let Self { + socket_addr, + flow_spec_info, + } = self; // Setting this to None means we'll be using cargo-leptos and its env vars let conf = leptos::get_configuration(None).await.unwrap(); let leptos_options = conf.leptos_options; let socket_addr = socket_addr.unwrap_or(leptos_options.site_addr); - let routes = leptos_axum::generate_route_list(|| view! { }); + let routes = leptos_axum::generate_route_list(move || view! { }); stream::iter(crate::assets::ASSETS.into_iter()) .map(Result::<_, WebiError>::Ok) @@ -73,6 +82,7 @@ impl WebiOutput { }) .await?; + let flow_spec_info = flow_spec_info.clone(); let router = Router::new() // serve the pkg directory .nest_service( @@ -82,7 +92,12 @@ impl WebiOutput { // serve the `webi` directory .nest_service("/webi", ServeDir::new(Path::new("webi"))) // serve the SSR rendered homepage - .leptos_routes(&leptos_options, routes, move || view! { }) + .leptos_routes_with_context( + &leptos_options, + routes, + move || leptos::provide_context(flow_spec_info.clone()), + move || view! { }, + ) .with_state(leptos_options); let listener = tokio::net::TcpListener::bind(socket_addr) diff --git a/doc/src/ideas.md b/doc/src/ideas.md index bb48d29c4..6bed333c0 100644 --- a/doc/src/ideas.md +++ b/doc/src/ideas.md @@ -261,6 +261,60 @@ Should we combine all 3 into `FnCtx`? It would make `FnCtx` type parameterized o +
+16. Combine data and params{,_partial} into FnCtx +
+ +`Item` functions take in `FnCtx`, `data`, and item `params` as separate arguments. + +This was done to: + +* Reduce the additional layer to get `Item::Params`, or `Item::ParamsPartial`. +* Avoid progress sender from being passed in to function that didn't need it. + +However, functions don't necessarily need runtime `fn_ctx` or `data`, making it noise in the signature. + +Should we combine all 3 into `FnCtx`? It would make `FnCtx` type parameterized over `Params` and `ParamsPartial`. + +
+
+ +
+17. Style edges / items red when an error occurs. +
+ +When we hit an error, can we go through parameters / states to determine whether the error is to do with an item itself, or a link between the item and its predecessor? + +Then style that link red. + +
+
+ +
+18. Markdown text instead of Presentable. +
+ +Instead of requiring developers to `impl Presentable for` all the different types that use, and use different `Presentable` methods, we could require them to implement `Display`, using a markdown string. + +Then, for different `OutputWrite` implementations, we would do something like this: + +* `CliOutput`: `syntect` highlight things. +* `WebiOutput`: Use commonmark to generate HTML elements, and with 19, use that as part of each item node's content. + +
+
+ +
+19. HTML with SVG arrows and flexbox instead of dot. +
+ +Instead of using `dot`, we just use flexbox and generate arrows between HTML `div`s. + +This trades + +
+
+ ## Notes diff --git a/examples/envman/src/main.rs b/examples/envman/src/main.rs index 8114700a3..6f41a30a2 100644 --- a/examples/envman/src/main.rs +++ b/examples/envman/src/main.rs @@ -34,10 +34,12 @@ cfg_if::cfg_if! { } else if #[cfg(feature = "ssr")] { // web server use envman::model::EnvManError; - use peace::webi::output::WebiOutput; #[cfg(not(feature = "error_reporting"))] pub fn main() -> Result<(), EnvManError> { + use envman::flows::EnvDeployFlow; + use peace::webi::output::WebiOutput; + let runtime = tokio::runtime::Builder::new_multi_thread() .thread_name("main") .thread_stack_size(3 * 1024 * 1024) @@ -46,9 +48,12 @@ cfg_if::cfg_if! { .build() .map_err(EnvManError::TokioRuntimeInit)?; - let webi_output = WebiOutput::new(None); - runtime.block_on(async move { + let flow = EnvDeployFlow::flow().await?; + let flow_spec_info = flow.flow_spec_info(); + + let webi_output = WebiOutput::new(None, flow_spec_info); + webi_output.start().await.map_err(EnvManError::from) }) } diff --git a/examples/envman/src/main_cli.rs b/examples/envman/src/main_cli.rs index 0c8ab6da5..8d479e319 100644 --- a/examples/envman/src/main_cli.rs +++ b/examples/envman/src/main_cli.rs @@ -128,9 +128,13 @@ async fn run_command( EnvManCommand::Clean => EnvCleanCmd::run(cli_output, debug).await?, #[cfg(feature = "web_server")] EnvManCommand::Web { address, port } => { + use envman::flows::EnvDeployFlow; use peace::webi::output::WebiOutput; - let webi_output = WebiOutput::new(Some(SocketAddr::from((address, port)))); + let flow = EnvDeployFlow::flow().await?; + let flow_spec_info = flow.flow_spec_info(); + let webi_output = + WebiOutput::new(Some(SocketAddr::from((address, port))), flow_spec_info); webi_output.start().await?; // WebServer::start(Some(SocketAddr::from((address, port)))).await? From 28792f61c666211d4168e5d96c9961d952f2251d Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Sat, 17 Feb 2024 22:34:08 +1300 Subject: [PATCH 14/38] Use `GraphStyle::Circle` to render `FlowSpecInfo` graph. --- crate/webi_components/src/flow_graph.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crate/webi_components/src/flow_graph.rs b/crate/webi_components/src/flow_graph.rs index adabb9174..19fbd1996 100644 --- a/crate/webi_components/src/flow_graph.rs +++ b/crate/webi_components/src/flow_graph.rs @@ -43,7 +43,10 @@ pub fn FlowGraph() -> impl IntoView { #[leptos::server(endpoint = "/flow_graph")] pub async fn progress_dot_graph() -> Result> { - use dot_ix::{model::common::GraphvizDotTheme, rt::IntoGraphvizDotSrc}; + use dot_ix::{ + model::common::{graphviz_dot_theme::GraphStyle, GraphvizDotTheme}, + rt::IntoGraphvizDotSrc, + }; use peace_flow_model::FlowSpecInfo; let flow_spec_info = leptos::use_context::().ok_or_else(|| { @@ -53,6 +56,6 @@ pub async fn progress_dot_graph() -> Result Date: Sun, 18 Feb 2024 09:42:47 +1300 Subject: [PATCH 15/38] Draw `outcome_dot_graph` next to `progress_dot_graph`. --- crate/flow_model/src/flow_spec_info.rs | 4 +- crate/webi_components/src/flow_graph.rs | 61 +++++++++++++++++-------- 2 files changed, 42 insertions(+), 23 deletions(-) diff --git a/crate/flow_model/src/flow_spec_info.rs b/crate/flow_model/src/flow_spec_info.rs index 445b603c6..13fddf4d3 100644 --- a/crate/flow_model/src/flow_spec_info.rs +++ b/crate/flow_model/src/flow_spec_info.rs @@ -119,9 +119,7 @@ fn outcome_node_hierarchy( if let Some(child_node_hierarchy) = outcome_node_hierarchy(graph_info, visited, child_node_index) { - let item_spec_info = graph_info.node_weight(node_index).unwrap_or_else(|| { - panic!("`node_index`: `{node_index:?}` is invalid when accessing `flow_item_spec.graph_info`") - }); + let item_spec_info = &graph_info[child_node_index]; hierarchy.insert( item_spec_info_to_node_id(item_spec_info), child_node_hierarchy, diff --git a/crate/webi_components/src/flow_graph.rs b/crate/webi_components/src/flow_graph.rs index 19fbd1996..7ec28e733 100644 --- a/crate/webi_components/src/flow_graph.rs +++ b/crate/webi_components/src/flow_graph.rs @@ -1,46 +1,47 @@ use dot_ix::{model::common::DotSrcAndStyles, web_components::DotSvg}; use leptos::{ - component, create_signal, server_fn::error::NoCustomError, view, IntoView, ServerFnError, - SignalGet, SignalUpdate, Transition, + component, server_fn::error::NoCustomError, view, IntoView, ServerFnError, SignalGet, + Transition, }; /// Renders the flow graph. #[component] pub fn FlowGraph() -> impl IntoView { - let (count, set_count) = create_signal(0); - - let dot_source_resource = leptos::create_resource( + let progress_dot_resource = leptos::create_resource( || (), move |()| async move { progress_dot_graph().await.unwrap() }, ); - let progress_dot_graph = move || { - let progress_dot_graph = dot_source_resource + let progress_dot_graph = progress_dot_resource .get() .expect("Expected `progress_dot_graph` to always be generated successfully."); Some(progress_dot_graph) }; + let outcome_dot_resource = leptos::create_resource( + || (), + move |()| async move { outcome_dot_graph().await.unwrap() }, + ); + let outcome_dot_graph = move || { + let outcome_dot_graph = outcome_dot_resource + .get() + .expect("Expected `outcome_dot_graph` to always be generated successfully."); + + Some(outcome_dot_graph) + }; + view! { -
- -
-
-
- "hello leptos!" { move || count.get() } -
- -
-
"Loading graph..."

}> - +
+ + +
} } +/// Returns the graph representing item execution progress. #[leptos::server(endpoint = "/flow_graph")] pub async fn progress_dot_graph() -> Result> { use dot_ix::{ @@ -59,3 +60,23 @@ pub async fn progress_dot_graph() -> Result Result> { + use dot_ix::{ + model::common::{graphviz_dot_theme::GraphStyle, GraphvizDotTheme}, + rt::IntoGraphvizDotSrc, + }; + use peace_flow_model::FlowSpecInfo; + + let flow_spec_info = leptos::use_context::().ok_or_else(|| { + ServerFnError::::ServerError("`FlowSpecInfo` was not set.".to_string()) + })?; + + let outcome_info_graph = flow_spec_info.into_outcome_info_graph(); + Ok(IntoGraphvizDotSrc::into( + &outcome_info_graph, + &GraphvizDotTheme::default().with_graph_style(GraphStyle::Boxes), + )) +} From 7d4a8a6481ad41c09018be7da20f8dd3a735233a Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Sun, 18 Feb 2024 12:45:24 +1300 Subject: [PATCH 16/38] Use `add_logic_edges` and `use_contains_edges` between items. --- .../empathetic_code_design/flow_definition.md | 4 ++-- doc/src/technical_concepts/function_graph.md | 2 +- doc/src/technical_concepts/function_graph/streaming.md | 2 +- doc/src/technical_concepts/item_graph.md | 2 +- examples/envman/src/flows/app_upload_flow.rs | 3 ++- examples/envman/src/flows/env_deploy_flow.rs | 6 +++--- workspace_tests/src/rt/cmds/ensure_cmd.rs | 4 ++-- 7 files changed, 12 insertions(+), 11 deletions(-) diff --git a/doc/src/learning_material/empathetic_code_design/flow_definition.md b/doc/src/learning_material/empathetic_code_design/flow_definition.md index bc0c5e342..ca09b3005 100644 --- a/doc/src/learning_material/empathetic_code_design/flow_definition.md +++ b/doc/src/learning_material/empathetic_code_design/flow_definition.md @@ -78,7 +78,7 @@ let flow = { S3ObjectItem::::new(item_id!("s3_object")).into(), ]); - graph_builder.add_edges([(a, b), (b, c)])?; + graph_builder.add_logic_edges([(a, b), (b, c)])?; graph_builder.build() }; @@ -153,7 +153,7 @@ digraph { ```diff - graph_builder.add_edges([ + graph_builder.add_logic_edges([ - (a, b), + (a, c), (b, c), diff --git a/doc/src/technical_concepts/function_graph.md b/doc/src/technical_concepts/function_graph.md index b4c07aea8..b0fbe2f48 100644 --- a/doc/src/technical_concepts/function_graph.md +++ b/doc/src/technical_concepts/function_graph.md @@ -115,7 +115,7 @@ let fn_graph = { // Define dependencies to control ordering. fn_graph_builder - .add_edges([ + .add_logic_edges([ (fn_id1, fn_id2), (fn_id1, fn_id3), (fn_id2, fn_id4), diff --git a/doc/src/technical_concepts/function_graph/streaming.md b/doc/src/technical_concepts/function_graph/streaming.md index a52a54b14..73070467a 100644 --- a/doc/src/technical_concepts/function_graph/streaming.md +++ b/doc/src/technical_concepts/function_graph/streaming.md @@ -265,7 +265,7 @@ This can be seen by timing the executions: # # // Define dependencies to control ordering. # fn_graph_builder -# .add_edges([ +# .add_logic_edges([ # (fn_id1, fn_id2), # (fn_id1, fn_id3), # (fn_id2, fn_id4), diff --git a/doc/src/technical_concepts/item_graph.md b/doc/src/technical_concepts/item_graph.md index e0f3df92e..ca9d196e0 100644 --- a/doc/src/technical_concepts/item_graph.md +++ b/doc/src/technical_concepts/item_graph.md @@ -60,7 +60,7 @@ let graph = { Item3::new().into(), Item4::new().into(), ]); - graph_builder.add_edges([ + graph_builder.add_logic_edges([ (id_1, id_3), (id_2, id_3), (id_2, id_4), diff --git a/examples/envman/src/flows/app_upload_flow.rs b/examples/envman/src/flows/app_upload_flow.rs index 3f5b560b5..52d283544 100644 --- a/examples/envman/src/flows/app_upload_flow.rs +++ b/examples/envman/src/flows/app_upload_flow.rs @@ -34,7 +34,8 @@ impl AppUploadFlow { S3ObjectItem::::new(item_id!("s3_object")).into(), ]); - graph_builder.add_edges([(a, c), (b, c)])?; + graph_builder.add_logic_edge(a, c)?; + graph_builder.add_contains_edge(b, c)?; graph_builder.build() }; diff --git a/examples/envman/src/flows/env_deploy_flow.rs b/examples/envman/src/flows/env_deploy_flow.rs index 9e53e3fd8..d79f74520 100644 --- a/examples/envman/src/flows/env_deploy_flow.rs +++ b/examples/envman/src/flows/env_deploy_flow.rs @@ -52,15 +52,15 @@ impl EnvDeployFlow { S3ObjectItem::::new(item_id!("s3_object")).into(), ]); - graph_builder.add_edges([ + graph_builder.add_logic_edges([ (app_download_id, app_extract_id), (iam_policy_item_id, iam_role_item_id), (iam_role_item_id, instance_profile_item_id), // Download the file before uploading it. (app_download_id, s3_object_id), - // Create the bucket before uploading to it. - (s3_bucket_id, s3_object_id), ])?; + // Create the bucket before uploading to it. + graph_builder.add_contains_edge(s3_bucket_id, s3_object_id)?; graph_builder.build() }; diff --git a/workspace_tests/src/rt/cmds/ensure_cmd.rs b/workspace_tests/src/rt/cmds/ensure_cmd.rs index 45f81fc5b..f36bf8da9 100644 --- a/workspace_tests/src/rt/cmds/ensure_cmd.rs +++ b/workspace_tests/src/rt/cmds/ensure_cmd.rs @@ -1520,7 +1520,7 @@ async fn states_current_not_serialized_on_states_discover_cmd_block_fail() }) .into(), ); - graph_builder.add_edge(vec_copy_fn_id, mock_fn_id)?; + graph_builder.add_logic_edge(vec_copy_fn_id, mock_fn_id)?; graph_builder.build() }; let flow = Flow::new(FlowId::new(crate::fn_name_short!())?, graph); @@ -1704,7 +1704,7 @@ async fn states_current_is_serialized_on_apply_exec_cmd_block_interrupt() let mut graph_builder = ItemGraphBuilder::::new(); let vec_copy_id = graph_builder.add_fn(VecCopyItem::default().into()); let mock_id = graph_builder.add_fn(MockItem::<()>::default().into()); - graph_builder.add_edge(vec_copy_id, mock_id)?; + graph_builder.add_logic_edge(vec_copy_id, mock_id)?; graph_builder.build() }; let flow = Flow::new(FlowId::new(crate::fn_name_short!())?, graph); From 413c5162b6729127915e64c847a17320b3397372 Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Wed, 21 Feb 2024 08:32:28 +1300 Subject: [PATCH 17/38] Add `diagrams.md` technical concept, with most info in `outcome.md`. --- doc/src/SUMMARY.md | 3 + doc/src/technical_concepts/diagrams.md | 8 + .../technical_concepts/diagrams/outcome.md | 146 +++++++++++++++++ .../outcome/2024-02-18_outcome_diagram.svg | 84 ++++++++++ .../outcome/2024-02-18_outcome_diagram_2.svg | 148 +++++++++++++++++ .../technical_concepts/diagrams/progress.md | 3 + .../progress/2024-02-18_progress_diagram.svg | 154 ++++++++++++++++++ doc/src/technical_concepts/item.md | 4 +- 8 files changed, 548 insertions(+), 2 deletions(-) create mode 100644 doc/src/technical_concepts/diagrams.md create mode 100644 doc/src/technical_concepts/diagrams/outcome.md create mode 100644 doc/src/technical_concepts/diagrams/outcome/2024-02-18_outcome_diagram.svg create mode 100644 doc/src/technical_concepts/diagrams/outcome/2024-02-18_outcome_diagram_2.svg create mode 100644 doc/src/technical_concepts/diagrams/progress.md create mode 100644 doc/src/technical_concepts/diagrams/progress/2024-02-18_progress_diagram.svg diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index bd259e2bc..61901ebd5 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -33,6 +33,9 @@ - [Output](technical_concepts/output.md) - [Execution Progress](technical_concepts/output/execution_progress.md) - [Presentation](technical_concepts/output/presentation.md) + - [Diagrams](technical_concepts/diagrams.md) + - [Progress](technical_concepts/diagrams/progress.md) + - [Outcome](technical_concepts/diagrams/outcome.md) - [Workspace](technical_concepts/workspace.md) - [Commands](technical_concepts/commands.md) - [Scopes](technical_concepts/commands/scopes.md) diff --git a/doc/src/technical_concepts/diagrams.md b/doc/src/technical_concepts/diagrams.md new file mode 100644 index 000000000..677721eb5 --- /dev/null +++ b/doc/src/technical_concepts/diagrams.md @@ -0,0 +1,8 @@ +# Diagrams + +This section explores how various diagrams help to provide clarity to the user for: + +* **Progress:** What's going on. +* **Outcome:** What does it look like. +* **Interaction:** What actions can be taken. +* **Understanding:** Hiding / providing layers of detail. diff --git a/doc/src/technical_concepts/diagrams/outcome.md b/doc/src/technical_concepts/diagrams/outcome.md new file mode 100644 index 000000000..6391bf261 --- /dev/null +++ b/doc/src/technical_concepts/diagrams/outcome.md @@ -0,0 +1,146 @@ +# Outcome + +An outcome diagram should: + +* Show the "physical" items that exist / yet-to-exist / to-be-cleaned. +* Show which steps are linked to it, e.g. clicking on a file shows the steps that write to / read from the file. + + +## Determining Information for Rendering + +To render the outcome diagram, we need to deduce the physical things from `Item`s, and determine: + +1. **Source:** where data comes from, whether completely from parameters, or whether parameters are a reference to the data. +2. **Destination:** where data moves to or the system work is done to. +3. Whether the source or destination are declared in parameters. + +As of 2024-02-18, `Item::Params` is a single type, which would take in both **source** and **destination** parameters, so we cannot (realistically) determine the source/destination/host/cloud from the `Item::Params` type. + + +## What Rendering Makes Sense + +Conceptually, `Item`s can be thought of either an edge or a node: + +* **Edge:** The item represents an action / work to between the source(s) and the destination(s). +* **Node:** The item represents the destination thing. + + +### Naive + +Consider the following diagram, which is the first attempt at rendering an outcome diagram on 2024-02-18 -- this uses edges / hierarchy to draw nodes and edges: + + + +#### Notes + +1. It is not clear where the `app_download` `Item` transfers the file from, or to. +2. It is not clear that the `iam_policy`, `iam_role`, and `instance_profile` are defined in AWS. +3. The links between `iam_policy`, `iam_role`, and `instance_profile` should probably be reversed, to indicate what references what. +4. The `s3_object` item transfers the downloaded application file, to the S3 bucket created by `s3_bucket`, but it isn't clear whether we should be highlighting the link, or the nested node. +5. If we highlight the link, then note that it is a forward link (data is pushed), compared to point 3 which are backward links (references). + + +### Hosts, Realms, and Edges + +If we could show: + +1. The hosts of all network communication +2. The "realm" where resources live in, which may be a cloud provider or a region within +3. A node for each resource that is created/modified/deleted +4. For each item, the edges and nodes that interact + +then what is happening becomes slightly clearer: + +[dot_ix](https://azriel.im/dot_ix/?src=LQhQBMEsCcFMGMAukD2A7AXAAgG62svAIYA2oAFpPkdPOQJ4ahZZEDuAzkyy5EQLYB9AA4oSkeIywBvAL7MefIdDGxschbzQdERNPFgiVAM0gk1M%2BTywcAzIIBGAV3gBrWIm7Wb9lA4BWCJ6WoAokKMQk5Cg6XixEwsKC4ChsaOFE4OpWPAlJsAAeiNBESNmhLADmkIjkTg5xWNW19YJ5ggBekMLloGgo4IaQaMYxXuGR0bHePNJYsPwo-pDYgLwbgN07ADRYaAIWAEQTpFOI%2B9szWIMc8Nj7AJooTtAAPA7QWAD0AHzwKPzCTkQ%2BH2WBy7C4FwucwWSxWWEAgGSAeD-trt%2BAdwWdITMrjcsPsAIL8IgddBYADqsAcr2g3wAyvgcBJYBwQTlmnUGliWNDFss1oBMHZRe1u7PqmK5ONuAHEahzWQpRQ42olOt1sNz5ry4atAIM7QrRtzyADousJ5fEVSk0hkst4ebC1oBTnf16MtqXSKEy4p4krxxtN5tYKsKxVKwTtmodWFWgCGdl2G4NFEpIb0%2B5m4-YfFDCRAfPKBpQiMQSKQze186OAXZ343jC6JxJJU2nrrcAKIAYQATBhqZ8vviSOE2L26bYsAAlWBe0EKQsqczqqGRiurQAMuzX9nPVE3rL79h3O1gAJL4gCyI6%2B9ZLF6IiF0dDRaFOM60Oj0BiMKFMC5ky7hgB4NwB3-eRHZhVrbRdH0QxhBMMxYExPcDywYY3ygi951gVg71KchH2fHI7EcFx3HDMs-2wQA%2BDcAYr2N0I5w3A8HcWD3QZFmAGhkGMMMWRfHxBD8QIykhcttT1UCDTxQiBKCJjLnTBNhBNbp5VAWBwEqZkvHaK0PUyQRlXyJMw2wABtbT3RtbZ2hDZNEAAXVnAQiwbeh9K3H8TLrYtJG2dzYAc3gnIwtyIPfaDYI8vzfNCqDP2-fyFHM61PXAfSpICIJTKS3TwG2dLBPsxyhCvSQ0t8DKhM8pySvoPLyoKgKmllVp2lNfTrKMyrFQM1VhCsxNQyQBzQF0Mw2GGVL4BIIgOA4TSFDUjTklgLinBIRAIUocBBjQRK3WSvTBHyzKsC%2BYBNCwEyADJL1vcg7IwHQVHcYBOwu67btqB6npQF6JsgSoUGAAA2AAGUH3pu%2Bt6EBtBvuKX7YGAf7AZB8HIc%2B%2B6MBMn6XvAGbyBoEpGFsRqWA%2B4Q7oevRICJIFgBxhG8YJr9jDmxBgEWPBBAAFg4QRxDQKdoEEYZTDQGoEusCmxBh9AHuiPBoAwb8SGRtAoFRnn0elqGqYwRX8EepmkZRoHtYh3XL1l2GHtGeAnC4VX1c1oGAFYdZ4Cn9ftx2MEeRBBaRt6rcpr6VYiP2A6Dl2AaB2xPfJvXw99rho%2BGJH8Y4cg1IxsOsdTjAVCcDW1OAAoyGsB3oA4FBoGAURhiBaAKmQ6rvNco76pOradsSzhsDOvOfbMNWBAcfBgAARkTy7k6x3Gkan4fw8X9j%2BAn%2BuLZXrHGeezOCaJohGE7Mm58xhWUCVlXR-XzfgA9nfL%2Bvtfx8nj3La9%2Be7cjp3b-oWAg5UivVnt7FOv9jb7zvu-UB38I4OzToCGOIcv4X3gVHJBGdgAAKAWwYACdP5JzQYXdOQtgBZxzuAJ%2B6CuDF1LuAculceDV1rvXRuT58CtyOFEGIwQh6hxHoOYADgSBOCXrAtBa9l4CNXibYRoikbbxkbvNeFCj4nzPmArGhtlbOxEWIh%2BhDz752fkbNe%2BikYf2oYXZ2HBXD0BAUYrRP8EGQMRvIgxVjlEuIwYHLBKCiEmJof7TBZC7EOIIdYiBpCD7Z1zt44JdDBgMIrhdFhdcG4oCbpwhUzVOT8NQUE2x016YzycXAqR1C14cBKYoiRQS97uLUdAYmGBT7UJ0TfIRNTbyWPKWgzp1TamGP6UUiBztKjE0cVE1xQzekjJmb45BizEF%2BLIZM4%2B%2BD6k%2B2iaE2JlCVlF0ePQxhaSnisMydkluoB4CzUHudFgAABdw9BjAlDRBwGwciKGs3ZpzK%2BmFpAXVBgAUlmF8qBPzjBsw8NgHmAAOAA3LxFgZSwVzFUSzaF7NsCg2RTkWQQA&css=AIawpgngZgTghgWzAZwATIC4wPbgLQAmcyAFtlFMmBngtgG5ioDeAUKqgAwCkHL6WXGELEyFKhgBcqACwAOANyoAvu1QBGTj36Yc%2BIqXKVq0zktXKgA) + + + +#### Notes + +1. There is only one level of detail. +2. It is useful to have expandable detail, e.g. hide a full URL, and allow the user to expand it if they need to. +3. It is useful to show and hide animated edges while that step is in progress. + + +### Technology for Rendering + +Currently: + +1. Graphviz `dot` is used to render the SVG. +2. Tailwind CSS is used to define and generate styles. +3. `leptos` is used with `axum` to serve the graph in a web application. +4. `dot_ix` connects those together. + +There are other use cases and desirable features that are difficult to implement with the current technology, namely: + +1. Rendering the outcome on a command line interface. +2. Richer formatting, also by item implementors. +3. Controllable / predictable / consistent node positioning. +4. Serializing the outcome rendering, for later display. +5. Diffing two outcome diagrams. +6. Rendering errors and recovery hints. + + +#### Command Line Interface + +Rendering the outcome on the CLI is not really necessary when the user is able to use a web browser to see the outcome. If the user is connected to a server via SSH, the tool could technically serve the web interface locally, and an SSH tunnel used to forward the traffic. + +[`dioxus`] may allow us to have single source for both the web and CLI, but this is not known for sure -- [`plasmo`], the crate that takes HTML and renders it on the terminal does not handle `` elements on the terminal, so exploration is needed to determine if this is suitable. + + +[`dioxus`]: https://github.com/DioxusLabs/dioxus +[`plasmo`]: https://github.com/DioxusLabs/dioxus/tree/master/packages/plasmo + + +#### Rich Formatting + +For richer formatting, the `Presentable` trait was intended to capture this. However, serialization of different types makes the code complex -- I haven't figured out a way to transfer arbitrary types across the server to a client. + +One option is to use markdown, and transfer the plain markdown to the target output, which can render it in its own means, e.g. [`syntect`] for CLI, and [`comrak`] or [`pulldown-cmark`] (what `mdbook` uses) to generate HTML for the web. + +We would have to automatically nest markdown by indenting inner types' markdown. + +[`syntect`]: https://github.com/trishume/syntect +[`pulldown-cmark`]: https://github.com/pulldown-cmark/pulldown-cmark +[`layout`]: https://github.com/nadavrot/layout + + +#### Controllable Node Positioning + +[`layout`] is a Rust port of a subset of `dot`. Essentially there are no HTML-like labels, but it is written in Rust. It likely has consistent node positioning, so using it over `dot` has that advantage, with the added benefits of performance and portability. [`vizdom`] uses it to generate graphs in real time. + +Instead of using a `dot`-like library, generating elements with a flexbox layout, and drawing arrows may be the way to go. + + +[`comrak`]: https://hrzn.ee/kivikakk/comrak + + +#### Serializing Outcome Rendering + +As long as we have a serializable form, which is *stable*, we can re-render the diagram later. + +This means all information about the flow, parameters, and errors need to be serializable. + + +#### Diffing Outcome Diagrams + +[`vizdom`] does this really nicely with `dot` graphs, see its examples. + +Should we do a visual diff? Or clever node matching, then a styling diff? + + +[`vizdom`]: https://www.vizdom.dev/ + + +#### Rendering Errors and Recovery Hints + +For rendering errors, we need to know whether the error is to do with: + +* a host / a function run by a host that failed +* a connection between hosts + +then styling it. + +For recovery hints, we need to make it clear where the error shown on the the outcome diagram is related to input parameters, whether it is a file, or a value produced by a host. + +If we are using `dot`-like or a CSS-enabled technology, then we can style the relevant edge / node with Tailwind CSS styles. diff --git a/doc/src/technical_concepts/diagrams/outcome/2024-02-18_outcome_diagram.svg b/doc/src/technical_concepts/diagrams/outcome/2024-02-18_outcome_diagram.svg new file mode 100644 index 000000000..4ac014700 --- /dev/null +++ b/doc/src/technical_concepts/diagrams/outcome/2024-02-18_outcome_diagram.svg @@ -0,0 +1,84 @@ + + + + + + + +G + +cluster_s3_bucket + +s3_bucket + + + +app_download + +app_download + + + +app_extract + +app_extract + + + +app_download->app_extract + + + + + +s3_object + +s3_object + + + +app_download->s3_object + + + + + +iam_policy + +iam_policy + + + +iam_role + +iam_role + + + +iam_policy->iam_role + + + + + +instance_profile + +instance_profile + + + +iam_role->instance_profile + + + + + diff --git a/doc/src/technical_concepts/diagrams/outcome/2024-02-18_outcome_diagram_2.svg b/doc/src/technical_concepts/diagrams/outcome/2024-02-18_outcome_diagram_2.svg new file mode 100644 index 000000000..d9afd5128 --- /dev/null +++ b/doc/src/technical_concepts/diagrams/outcome/2024-02-18_outcome_diagram_2.svg @@ -0,0 +1,148 @@ + + + + + + + +G + +cluster_aws + +☁️ +aws +Amazon Web +Services + + +cluster_s3_bucket + +🪣 +s3_bucket +demo-artifacts + + +cluster_localhost + +💻 +localhost +Your +computer + + +cluster_github + +🐙 +github +Github + + + +iam_policy + +📝 +iam_policy +EC2: +Allow +S3 Read + + + +iam_role + +🔰 +iam_role +EC2 IAM +policy +attachment + + + + + +s3_object + +📁 +s3_object +app.zip + + + + + +instance_profile + +🏷️ +instance_profile +EC2 instance +role attachment + + + + + +app_download + +📥 +app_download +app.zip + + + +app_download->s3_object + + + + + +app_extract + +📂 +app_extract +/opt/app + + + + + +github_app_zip + +📁 +app.zip + + + + + diff --git a/doc/src/technical_concepts/diagrams/progress.md b/doc/src/technical_concepts/diagrams/progress.md new file mode 100644 index 000000000..1536e7b11 --- /dev/null +++ b/doc/src/technical_concepts/diagrams/progress.md @@ -0,0 +1,3 @@ +# Progress + + diff --git a/doc/src/technical_concepts/diagrams/progress/2024-02-18_progress_diagram.svg b/doc/src/technical_concepts/diagrams/progress/2024-02-18_progress_diagram.svg new file mode 100644 index 000000000..b39145e28 --- /dev/null +++ b/doc/src/technical_concepts/diagrams/progress/2024-02-18_progress_diagram.svg @@ -0,0 +1,154 @@ + + + + + + + +G + +cluster_app_download + + + +cluster_app_extract + + + +cluster_s3_bucket + + + +cluster_s3_object + + + +cluster_iam_policy + + + +cluster_iam_role + + + +cluster_instance_profile + + + + +app_download + + + + +app_extract + + + + +app_download->app_extract + + + + + +s3_object + + + + +app_download->s3_object + + + + + +app_download_text + +app_download + + + +app_extract_text + +app_extract + + + +iam_policy + + + + +iam_role + + + + +iam_policy->iam_role + + + + + +iam_policy_text + +iam_policy + + + +instance_profile + + + + +iam_role->instance_profile + + + + + +iam_role_text + +iam_role + + + +instance_profile_text + +instance_profile + + + +s3_bucket + + + + +s3_bucket->s3_object + + + + + +s3_bucket_text + +s3_bucket + + + +s3_object_text + +s3_object + + + diff --git a/doc/src/technical_concepts/item.md b/doc/src/technical_concepts/item.md index 52eebf64b..7f7e50c12 100644 --- a/doc/src/technical_concepts/item.md +++ b/doc/src/technical_concepts/item.md @@ -4,9 +4,9 @@ An **item** is something that can be created, inspected, and cleaned up by automation. -An itemification defines data types and logic to manage that item. +"Itemification" is to define data types and logic to manage that item. -The [`Item`][`Item`] and associated types are how consumers integrate with the Peace framework. Consumers provide a unique ID, data types, and functions, which will be selectively executed by the framework to provide lean, robust automation, and a good user experience. +The [`Item`][`Item`] trait and associated types are how consumers integrate with the Peace framework. Consumers provide a unique ID, data types, and functions, which will be selectively executed by the framework to provide lean, robust automation, and a good user experience. This logical breakdown guides automation developers to structure logic to handle cases that are not normally considered of when writing automation. Combined with trait requirements on data types, the framework is able to provide commands, workflow optimizations, and understandable output to ensure a pleasant automation experience. From 60d68b356470e90a8bf91edbf7f3e5ac18ee7fb3 Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Fri, 23 Feb 2024 18:47:47 +1300 Subject: [PATCH 18/38] Update dependency versions. --- Cargo.toml | 34 ++++++++++++++--------------- examples/download/Cargo.toml | 18 ++++++++-------- examples/envman/Cargo.toml | 42 ++++++++++++++++++------------------ 3 files changed, 47 insertions(+), 47 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index da5c345a4..808a8aa16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -141,49 +141,49 @@ axum = "0.7.4" base64 = "0.21.7" bytes = "1.5.0" cfg-if = "1.0.0" -chrono = { version = "0.4.33", default-features = false, features = ["clock", "serde"] } +chrono = { version = "0.4.34", default-features = false, features = ["clock", "serde"] } console = "0.15.8" derivative = "2.2.0" diff-struct = "0.5.3" downcast-rs = "1.2.0" -dot_ix = { path = "/mnt/data/work/github/azriel91/dot_ix" } +dot_ix = "0.2.0" dyn-clone = "1.0.16" enser = "0.1.4" -erased-serde = "0.4.2" +erased-serde = "0.4.3" fn_graph = { version = "0.12.0", features = ["async", "interruptible", "resman"] } futures = "0.3.30" heck = "0.4.1" -indexmap = "2.1.0" -indicatif = "0.17.7" +indexmap = "2.2.3" +indicatif = "0.17.8" interruptible = "0.2.1" leptos = { version = "0.6" } leptos_axum = "0.6" leptos_meta = { version = "0.6" } leptos_router = { version = "0.6" } -libc = "0.2.152" -miette = "5.10.0" +libc = "0.2.153" +miette = "7.1.0" pretty_assertions = "1.4.0" proc-macro2 = "1.0.78" quote = "1.0.35" raw_tty = "0.1.0" -reqwest = "0.11.23" +reqwest = "0.11.24" resman = "0.17.0" -serde = "1.0.196" -serde-wasm-bindgen = "0.6.3" -serde_json = "1.0.112" -serde_yaml = "0.9.30" -syn = "2.0.48" +serde = "1.0.197" +serde-wasm-bindgen = "0.6.4" +serde_json = "1.0.114" +serde_yaml = "0.9.32" +syn = "2.0.50" tar = "0.4.40" -tempfile = "3.9.0" -thiserror = "1.0.56" +tempfile = "3.10.0" +thiserror = "1.0.57" tokio = "1.36" tokio-util = "0.7.10" tower-http = "0.5.1" tynm = "0.1.10" type_reg = { version = "0.7.0", features = ["debug", "untagged", "ordered"] } url = "2.5.0" -wasm-bindgen = "0.2.90" -web-sys = "0.3.67" +wasm-bindgen = "0.2.91" +web-sys = "0.3.68" [patch.crates-io] fn_graph = { path = "/mnt/data/work/github/azriel91/fn_graph" } diff --git a/examples/download/Cargo.toml b/examples/download/Cargo.toml index 093d5463b..fd0658565 100644 --- a/examples/download/Cargo.toml +++ b/examples/download/Cargo.toml @@ -20,21 +20,21 @@ crate-type = ["cdylib", "rlib"] [dependencies] peace = { path = "../..", default-features = false, features = ["cli"] } peace_items = { path = "../../items", features = ["file_download"] } -thiserror = "1.0.56" +thiserror = "1.0.57" url = { version = "2.5.0", features = ["serde"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -clap = { version = "4.4.18", features = ["derive"] } -tokio = { version = "1.35.1", features = ["net", "time", "rt"] } +clap = { version = "4.5.1", features = ["derive"] } +tokio = { version = "1.36.0", features = ["net", "time", "rt"] } [target.'cfg(target_arch = "wasm32")'.dependencies] console_error_panic_hook = "0.1.7" -serde-wasm-bindgen = "0.6.3" -tokio = "1.35.1" -wasm-bindgen = "0.2.90" -wasm-bindgen-futures = "0.4.40" -js-sys = "0.3.67" -web-sys = "0.3.67" +serde-wasm-bindgen = "0.6.4" +tokio = "1.36.0" +wasm-bindgen = "0.2.91" +wasm-bindgen-futures = "0.4.41" +js-sys = "0.3.68" +web-sys = "0.3.68" [features] default = [] diff --git a/examples/envman/Cargo.toml b/examples/envman/Cargo.toml index 711949385..5842f3f06 100644 --- a/examples/envman/Cargo.toml +++ b/examples/envman/Cargo.toml @@ -18,21 +18,21 @@ test = false crate-type = ["cdylib", "rlib"] [dependencies] -aws-config = { version = "1.1.4", optional = true } -aws-sdk-iam = { version = "1.12.0", optional = true } -aws-sdk-s3 = { version = "1.14.0", optional = true } -aws-smithy-types = { version = "1.1.4", optional = true } # used to reference error type, otherwise not recommended for direct usage +aws-config = { version = "1.1.6", optional = true } +aws-sdk-iam = { version = "1.14.0", optional = true } +aws-sdk-s3 = { version = "1.16.0", optional = true } +aws-smithy-types = { version = "1.1.7", optional = true } # used to reference error type, otherwise not recommended for direct usage base64 = { version = "0.21.7", optional = true } cfg-if = "1.0.0" -chrono = { version = "0.4.33", default-features = false, features = ["clock", "serde"], optional = true } +chrono = { version = "0.4.34", default-features = false, features = ["clock", "serde"], optional = true } derivative = { version = "2.2.0", optional = true } futures = { version = "0.3.30", optional = true } md5-rs = { version = "0.1.5", optional = true } # WASM compatible, and reads bytes as stream peace = { path = "../..", default-features = false } peace_items = { path = "../../items", features = ["file_download", "tar_x"] } -semver = { version = "1.0.21", optional = true } -serde = { version = "1.0.196", features = ["derive"] } -thiserror = { version = "1.0.56", optional = true } +semver = { version = "1.0.22", optional = true } +serde = { version = "1.0.197", features = ["derive"] } +thiserror = { version = "1.0.57", optional = true } url = { version = "2.5.0", features = ["serde"] } urlencoding = { version = "2.1.3", optional = true } whoami = { version = "1.4.1", optional = true } @@ -40,29 +40,29 @@ whoami = { version = "1.4.1", optional = true } # web_server # ssr axum = { version = "0.7.4", optional = true } -hyper = { version = "1.1.0", optional = true } -leptos = { version = "0.6.3", default-features = false, features = ["serde"] } -leptos_axum = { version = "0.6.3", optional = true } -leptos_meta = { version = "0.6.3", default-features = false } -leptos_router = { version = "0.6.3", default-features = false } +hyper = { version = "1.2.0", optional = true } +leptos = { version = "0.6.6", default-features = false, features = ["serde"] } +leptos_axum = { version = "0.6.6", optional = true } +leptos_meta = { version = "0.6.6", default-features = false } +leptos_router = { version = "0.6.6", default-features = false } tower = { version = "0.4.13", optional = true } tower-http = { version = "0.5.1", optional = true, features = ["fs"] } tracing = { version = "0.1.40", optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -clap = { version = "4.4.18", features = ["derive"], optional = true } -tokio = { version = "1.35.1", features = ["rt", "rt-multi-thread", "signal"], optional = true } +clap = { version = "4.5.1", features = ["derive"], optional = true } +tokio = { version = "1.36.0", features = ["rt", "rt-multi-thread", "signal"], optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] console_error_panic_hook = "0.1.7" console_log = { version = "1.0.0", features = ["color"] } log = "0.4.20" -serde-wasm-bindgen = "0.6.3" -tokio = "1.35.1" -wasm-bindgen = "0.2.90" -wasm-bindgen-futures = "0.4.40" -js-sys = "0.3.67" -web-sys = "0.3.67" +serde-wasm-bindgen = "0.6.4" +tokio = "1.36.0" +wasm-bindgen = "0.2.91" +wasm-bindgen-futures = "0.4.41" +js-sys = "0.3.68" +web-sys = "0.3.68" [features] default = [] From 4870f42a8b2cf0c5661a5ef42459c402009755dd Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Fri, 23 Feb 2024 19:22:04 +1300 Subject: [PATCH 19/38] Update code for `miette` breaking changes. --- crate/rt_model_core/src/error.rs | 4 ++-- items/file_download/src/file_download_apply_fns.rs | 6 +----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/crate/rt_model_core/src/error.rs b/crate/rt_model_core/src/error.rs index 4f9561d9f..77173bdda 100644 --- a/crate/rt_model_core/src/error.rs +++ b/crate/rt_model_core/src/error.rs @@ -226,7 +226,7 @@ pub enum Error { /// Source text to be deserialized. #[cfg(feature = "error_reporting")] #[source_code] - states_file_source: miette::NamedSource, + states_file_source: miette::NamedSource, /// Offset within the source text that the error occurred. #[cfg(feature = "error_reporting")] #[label("{}", error_message)] @@ -273,7 +273,7 @@ pub enum Error { /// Source text to be deserialized. #[cfg(feature = "error_reporting")] #[source_code] - params_specs_file_source: miette::NamedSource, + params_specs_file_source: miette::NamedSource, /// Offset within the source text that the error occurred. #[cfg(feature = "error_reporting")] #[label("{}", error_message)] diff --git a/items/file_download/src/file_download_apply_fns.rs b/items/file_download/src/file_download_apply_fns.rs index a1c9cef16..e61fc5743 100644 --- a/items/file_download/src/file_download_apply_fns.rs +++ b/items/file_download/src/file_download_apply_fns.rs @@ -161,11 +161,7 @@ where dest_offset_col + 1, ); // Add one to length because we are 1-based, not 0-based? - let length = SourceOffset::from_location( - &init_command_approx, - loc_line, - init_command_approx.len() - dest_offset_col + 1, - ); + let length = init_command_approx.len() - dest_offset_col + 1; SourceSpan::new(start, length) }; Err(FileDownloadError::DestFileCreate { From 2fa93eb509330f6e9aed853b6e420d4a070f0e70 Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Mon, 26 Feb 2024 18:53:46 +1300 Subject: [PATCH 20/38] Split `outcome.md` and add `html_flexbox.md`. --- doc/src/SUMMARY.md | 2 + .../technical_concepts/diagrams/outcome.md | 84 ------------ .../diagrams/outcome/html_flexbox.md | 17 +++ .../outcome/html_flexbox/example.html | 121 ++++++++++++++++++ .../diagrams/outcome/render_technology.md | 82 ++++++++++++ 5 files changed, 222 insertions(+), 84 deletions(-) create mode 100644 doc/src/technical_concepts/diagrams/outcome/html_flexbox.md create mode 100644 doc/src/technical_concepts/diagrams/outcome/html_flexbox/example.html create mode 100644 doc/src/technical_concepts/diagrams/outcome/render_technology.md diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index 61901ebd5..297c173ed 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -36,6 +36,8 @@ - [Diagrams](technical_concepts/diagrams.md) - [Progress](technical_concepts/diagrams/progress.md) - [Outcome](technical_concepts/diagrams/outcome.md) + - [Render Technology](technical_concepts/diagrams/outcome/render_technology.md) + - [HTML + Flexbox](technical_concepts/diagrams/outcome/html_flexbox.md) - [Workspace](technical_concepts/workspace.md) - [Commands](technical_concepts/commands.md) - [Scopes](technical_concepts/commands/scopes.md) diff --git a/doc/src/technical_concepts/diagrams/outcome.md b/doc/src/technical_concepts/diagrams/outcome.md index 6391bf261..e4cd517f2 100644 --- a/doc/src/technical_concepts/diagrams/outcome.md +++ b/doc/src/technical_concepts/diagrams/outcome.md @@ -60,87 +60,3 @@ then what is happening becomes slightly clearer: 1. There is only one level of detail. 2. It is useful to have expandable detail, e.g. hide a full URL, and allow the user to expand it if they need to. 3. It is useful to show and hide animated edges while that step is in progress. - - -### Technology for Rendering - -Currently: - -1. Graphviz `dot` is used to render the SVG. -2. Tailwind CSS is used to define and generate styles. -3. `leptos` is used with `axum` to serve the graph in a web application. -4. `dot_ix` connects those together. - -There are other use cases and desirable features that are difficult to implement with the current technology, namely: - -1. Rendering the outcome on a command line interface. -2. Richer formatting, also by item implementors. -3. Controllable / predictable / consistent node positioning. -4. Serializing the outcome rendering, for later display. -5. Diffing two outcome diagrams. -6. Rendering errors and recovery hints. - - -#### Command Line Interface - -Rendering the outcome on the CLI is not really necessary when the user is able to use a web browser to see the outcome. If the user is connected to a server via SSH, the tool could technically serve the web interface locally, and an SSH tunnel used to forward the traffic. - -[`dioxus`] may allow us to have single source for both the web and CLI, but this is not known for sure -- [`plasmo`], the crate that takes HTML and renders it on the terminal does not handle `` elements on the terminal, so exploration is needed to determine if this is suitable. - - -[`dioxus`]: https://github.com/DioxusLabs/dioxus -[`plasmo`]: https://github.com/DioxusLabs/dioxus/tree/master/packages/plasmo - - -#### Rich Formatting - -For richer formatting, the `Presentable` trait was intended to capture this. However, serialization of different types makes the code complex -- I haven't figured out a way to transfer arbitrary types across the server to a client. - -One option is to use markdown, and transfer the plain markdown to the target output, which can render it in its own means, e.g. [`syntect`] for CLI, and [`comrak`] or [`pulldown-cmark`] (what `mdbook` uses) to generate HTML for the web. - -We would have to automatically nest markdown by indenting inner types' markdown. - -[`syntect`]: https://github.com/trishume/syntect -[`pulldown-cmark`]: https://github.com/pulldown-cmark/pulldown-cmark -[`layout`]: https://github.com/nadavrot/layout - - -#### Controllable Node Positioning - -[`layout`] is a Rust port of a subset of `dot`. Essentially there are no HTML-like labels, but it is written in Rust. It likely has consistent node positioning, so using it over `dot` has that advantage, with the added benefits of performance and portability. [`vizdom`] uses it to generate graphs in real time. - -Instead of using a `dot`-like library, generating elements with a flexbox layout, and drawing arrows may be the way to go. - - -[`comrak`]: https://hrzn.ee/kivikakk/comrak - - -#### Serializing Outcome Rendering - -As long as we have a serializable form, which is *stable*, we can re-render the diagram later. - -This means all information about the flow, parameters, and errors need to be serializable. - - -#### Diffing Outcome Diagrams - -[`vizdom`] does this really nicely with `dot` graphs, see its examples. - -Should we do a visual diff? Or clever node matching, then a styling diff? - - -[`vizdom`]: https://www.vizdom.dev/ - - -#### Rendering Errors and Recovery Hints - -For rendering errors, we need to know whether the error is to do with: - -* a host / a function run by a host that failed -* a connection between hosts - -then styling it. - -For recovery hints, we need to make it clear where the error shown on the the outcome diagram is related to input parameters, whether it is a file, or a value produced by a host. - -If we are using `dot`-like or a CSS-enabled technology, then we can style the relevant edge / node with Tailwind CSS styles. diff --git a/doc/src/technical_concepts/diagrams/outcome/html_flexbox.md b/doc/src/technical_concepts/diagrams/outcome/html_flexbox.md new file mode 100644 index 000000000..a9c4cb3c5 --- /dev/null +++ b/doc/src/technical_concepts/diagrams/outcome/html_flexbox.md @@ -0,0 +1,17 @@ +# HTML + Flexbox + +{{#include html_flexbox/example.html}} + +
diagram source + +```html +{{#include html_flexbox/example.html}} +``` + +
+ +It is possible to produce a diagram in a similar style to dot using HTML elements and JS to draw an SVG arrow. The above uses [`leader-line`], which has been archived by the original author. + +It doesn't support adding an ID or CSS classes, so either we use its many configuration options, or we find another library which supports rendering arrows and setting classes. + +[`leader-line`]: https://www.npmjs.com/package/leader-line diff --git a/doc/src/technical_concepts/diagrams/outcome/html_flexbox/example.html b/doc/src/technical_concepts/diagrams/outcome/html_flexbox/example.html new file mode 100644 index 000000000..0f51b4b98 --- /dev/null +++ b/doc/src/technical_concepts/diagrams/outcome/html_flexbox/example.html @@ -0,0 +1,121 @@ + + +
+
+
🐙 Github
+
📁 app.zip
+
..
+
+
+
💻 Your computer
+
+
📥 app.zip
+
📂 /opt/app
+
+
+
+
☁️ Amazon Web Services
+
+
+
🪣 demo-artifacts
+
📁 app.zip
+
+
+
+
📝 EC2: Allow S3 Read
+
🔰 EC2 IAM policy attachment
+
🏷️ EC2 instance role attachment
+
+
+
+ + + diff --git a/doc/src/technical_concepts/diagrams/outcome/render_technology.md b/doc/src/technical_concepts/diagrams/outcome/render_technology.md new file mode 100644 index 000000000..e05040889 --- /dev/null +++ b/doc/src/technical_concepts/diagrams/outcome/render_technology.md @@ -0,0 +1,82 @@ +# Render Technology + +Currently: + +1. Graphviz `dot` is used to render the SVG. +2. Tailwind CSS is used to define and generate styles. +3. `leptos` is used with `axum` to serve the graph in a web application. +4. `dot_ix` connects those together. + +There are other use cases and desirable features that are difficult to implement with the current technology, namely: + +1. Rendering the outcome on a command line interface. +2. Richer formatting, also by item implementors. +3. Controllable / predictable / consistent node positioning. +4. Serializing the outcome rendering, for later display. +5. Diffing two outcome diagrams. +6. Rendering errors and recovery hints. + + +## Command Line Interface + +Rendering the outcome on the CLI is not really necessary when the user is able to use a web browser to see the outcome. If the user is connected to a server via SSH, the tool could technically serve the web interface locally, and an SSH tunnel used to forward the traffic. + +[`dioxus`] may allow us to have single source for both the web and CLI, but this is not known for sure -- [`plasmo`], the crate that takes HTML and renders it on the terminal does not handle `` elements on the terminal, so exploration is needed to determine if this is suitable. + + +[`dioxus`]: https://github.com/DioxusLabs/dioxus +[`plasmo`]: https://github.com/DioxusLabs/dioxus/tree/master/packages/plasmo + + +## Rich Formatting + +For richer formatting, the `Presentable` trait was intended to capture this. However, serialization of different types makes the code complex -- I haven't figured out a way to transfer arbitrary types across the server to a client. + +One option is to use markdown, and transfer the plain markdown to the target output, which can render it in its own means, e.g. [`syntect`] for CLI, and [`comrak`] or [`pulldown-cmark`] (what `mdbook` uses) to generate HTML for the web. + +We would have to automatically nest markdown by indenting inner types' markdown. + +[`syntect`]: https://github.com/trishume/syntect +[`pulldown-cmark`]: https://github.com/pulldown-cmark/pulldown-cmark +[`layout`]: https://github.com/nadavrot/layout + + +## Controllable Node Positioning + +[`layout`] is a Rust port of a subset of `dot`. Essentially there are no HTML-like labels, but it is written in Rust. It likely has consistent node positioning, so using it over `dot` has that advantage, with the added benefits of performance and portability. [`vizdom`] uses it to generate graphs in real time. + +Instead of using a `dot`-like library, generating elements with a flexbox layout, and drawing arrows may be the way to go. + + +[`comrak`]: https://hrzn.ee/kivikakk/comrak + + +## Serializing Outcome Rendering + +As long as we have a serializable form, which is *stable*, we can re-render the diagram later. + +This means all information about the flow, parameters, and errors need to be serializable. + + +## Diffing Outcome Diagrams + +[`vizdom`] does this really nicely with `dot` graphs, see its examples. + +Should we do a visual diff? Or clever node matching, then a styling diff? + + +[`vizdom`]: https://www.vizdom.dev/ + + +## Rendering Errors and Recovery Hints + +For rendering errors, we need to know whether the error is to do with: + +* a host / a function run by a host that failed +* a connection between hosts + +then styling it. + +For recovery hints, we need to make it clear where the error shown on the the outcome diagram is related to input parameters, whether it is a file, or a value produced by a host. + +If we are using `dot`-like or a CSS-enabled technology, then we can style the relevant edge / node with Tailwind CSS styles. From deefe1f5158b905e621eb5f410cc6a2d0c88b3ef Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Sun, 3 Mar 2024 16:55:39 +1300 Subject: [PATCH 21/38] Add `div_diag.md` notes. --- doc/src/SUMMARY.md | 1 + .../diagrams/outcome/div_diag.md | 96 +++++++++++++++++++ .../diagrams/outcome/render_technology.md | 8 ++ 3 files changed, 105 insertions(+) create mode 100644 doc/src/technical_concepts/diagrams/outcome/div_diag.md diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index 297c173ed..606128751 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -38,6 +38,7 @@ - [Outcome](technical_concepts/diagrams/outcome.md) - [Render Technology](technical_concepts/diagrams/outcome/render_technology.md) - [HTML + Flexbox](technical_concepts/diagrams/outcome/html_flexbox.md) + - [Div Diag](technical_concepts/diagrams/outcome/div_diag.md) - [Workspace](technical_concepts/workspace.md) - [Commands](technical_concepts/commands.md) - [Scopes](technical_concepts/commands/scopes.md) diff --git a/doc/src/technical_concepts/diagrams/outcome/div_diag.md b/doc/src/technical_concepts/diagrams/outcome/div_diag.md new file mode 100644 index 000000000..ed75b1621 --- /dev/null +++ b/doc/src/technical_concepts/diagrams/outcome/div_diag.md @@ -0,0 +1,96 @@ +# Div Diag + +Attempt at using `dot_ix` to generate a diagram using the same `InfoGraph` input model. + + +## Desireables + +1. Consistent layout. +2. Rendering arrows between nodes. +3. Styling nodes. +4. Styling arrows. +5. Consistent styling input with the `DotSvg` component. +6. Rendered SVG so that it can be uploaded as attachments and rendered inline, e.g. GitHub comments, mdbook. + + +## Solutioning + +### Options + +* **A:** Layout. +* **B:** Arrows. +* **C:** Styling. + +#### 🟢 A1: Flexbox + HTML to SVG Translation + +1. HTML elements with Flexbox, easy to integrate rendered Markdown +2. Translate HTML to SVG. + + +#### 🔴 A2: Render SVG directly + +1. Need to implement layout algorithm that a browser already does. + + e.g. Flexbox, or somehow computer width/height with font metrics, unicode widths, potentially images. + + +#### 🟡 B1: Arrow Overlay + +1. Separately draw arrows as SVG over the laid out diagram as an overlay layer. +2. Redraw arrows when layout changes. + + +#### 🟢 B2: Draw Arrows Inline + +1. Layout diagram. +2. Convert to SVG. +3. Draw arrows within the SVG. + + +#### 🟡 C1: CSS Utility Classes, e.g. TailwindCSS + +1. Take class names directly from consumer / user. +2. Apply those to the HTML / SVG elements. + +Requires element structure to be stable, and consumers to know the structure. + + +#### 🟢 C2: CSS Utility Classes Keyed + +1. Take in colours and border styles etc. +2. Prefix these with the appropriate structure, e.g. `[>path]:`, `hover:`, etc. +3. Apply these to the HTML / SVG elements. + +Provides a stabler interface for users, and less knowledge of internals needed. Also a bit more freedom for maintainers. + + +### Existing Tech + +#### HTML to SVG + +* ⚪ [`dom-to-svg`]\: Typescript. Still need to try this. +* ⚪ [`vertopal`]\: Python. Still need to try this. +* ⚪ Build one, with a cut down version of the diagram. + +Online tools have not been able to produce an accurate SVG rendering. + + +#### Drawing Arrows + +* 🟡 [`leader-line`]\: Generates SVG arrows, with good configuration options. Out of the box, the SVG structure is not suitable for TailwindCSS classes. +* 🟡 Modify [`leader-line`]\: Restructure the elements in the SVG, and add classes. +* ⚪ Build one, so that the elements are structured to match graphviz, and have classes added. + + +#### Styling + +* 🟡 [TailwindCSS]\: Versatile CSS library. See [utility-first] rationale. +* 🟢 [`encre-css`]\: This is a TailwindCSS compatible Rust library. + + +[`dom-to-svg`]: https://github.com/felixfbecker/dom-to-svg +[`encre-css`]: https://crates.io/crates/encre-css +[`leader-line`]: https://anseki.github.io/leader-line/ +[`vertopal`]: https://github.com/vertopal/vertopal-cli +[TailwindCSS]: https://tailwindcss.com/ +[utility-first]: https://tailwindcss.com/docs/utility-first diff --git a/doc/src/technical_concepts/diagrams/outcome/render_technology.md b/doc/src/technical_concepts/diagrams/outcome/render_technology.md index e05040889..721b3ce2b 100644 --- a/doc/src/technical_concepts/diagrams/outcome/render_technology.md +++ b/doc/src/technical_concepts/diagrams/outcome/render_technology.md @@ -80,3 +80,11 @@ then styling it. For recovery hints, we need to make it clear where the error shown on the the outcome diagram is related to input parameters, whether it is a file, or a value produced by a host. If we are using `dot`-like or a CSS-enabled technology, then we can style the relevant edge / node with Tailwind CSS styles. + + +## Tailwind CSS generation + +[`encre-css`] is likely what we will use, as it is TailwindCSS compatible. + + +[`encre-css`]: https://crates.io/crates/encre-css From 19fc5fdb2b64416c61dcff5eeb558c292d3a1d29 Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Tue, 5 Mar 2024 08:01:21 +1300 Subject: [PATCH 22/38] Remove redundant imports. --- crate/diff/src/equality.rs | 5 +---- crate/diff/src/tracked.rs | 5 +---- crate/params/src/mapping_fn_impl.rs | 4 ---- crate/resources/src/states/states_serde.rs | 2 +- 4 files changed, 3 insertions(+), 13 deletions(-) diff --git a/crate/diff/src/equality.rs b/crate/diff/src/equality.rs index e2da7074f..4981d3fb5 100644 --- a/crate/diff/src/equality.rs +++ b/crate/diff/src/equality.rs @@ -1,7 +1,4 @@ -use std::{ - cmp::{Ordering, PartialOrd}, - fmt, -}; +use std::{cmp::Ordering, fmt}; /// Represents whether a value is equal to another. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] diff --git a/crate/diff/src/tracked.rs b/crate/diff/src/tracked.rs index 32890b19c..eb94ad5e8 100644 --- a/crate/diff/src/tracked.rs +++ b/crate/diff/src/tracked.rs @@ -1,7 +1,4 @@ -use std::{ - cmp::PartialEq, - hash::{Hash, Hasher}, -}; +use std::hash::{Hash, Hasher}; use serde::{Deserialize, Serialize}; diff --git a/crate/params/src/mapping_fn_impl.rs b/crate/params/src/mapping_fn_impl.rs index 028c77547..a77966b06 100644 --- a/crate/params/src/mapping_fn_impl.rs +++ b/crate/params/src/mapping_fn_impl.rs @@ -417,10 +417,6 @@ macro_rules! try_arg_resolve { }; } -use arg_resolve; -use impl_mapping_fn_impl; -use try_arg_resolve; - // We can add more if we need to support more args. // // There is a compile time / Rust analyzer startup cost to it, so it's better to diff --git a/crate/resources/src/states/states_serde.rs b/crate/resources/src/states/states_serde.rs index 4077e58a2..4428cd2f3 100644 --- a/crate/resources/src/states/states_serde.rs +++ b/crate/resources/src/states/states_serde.rs @@ -1,6 +1,6 @@ //! Resources that track current and goal states, and state diffs. -use std::{fmt::Debug, iter::FromIterator, ops::Deref}; +use std::{fmt::Debug, ops::Deref}; use peace_core::ItemId; use serde::Serialize; From 9ed50d74df50dd43f45f3e20c6297265383410e4 Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Tue, 5 Mar 2024 08:31:42 +1300 Subject: [PATCH 23/38] Update `fn_graph` to `0.13.0` for `"graph_info"` feature. --- Cargo.toml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 808a8aa16..3e4a56e5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -150,7 +150,7 @@ dot_ix = "0.2.0" dyn-clone = "1.0.16" enser = "0.1.4" erased-serde = "0.4.3" -fn_graph = { version = "0.12.0", features = ["async", "interruptible", "resman"] } +fn_graph = { version = "0.13.0", features = ["async", "graph_info", "interruptible", "resman"] } futures = "0.3.30" heck = "0.4.1" indexmap = "2.2.3" @@ -184,6 +184,3 @@ type_reg = { version = "0.7.0", features = ["debug", "untagged", "ordered"] } url = "2.5.0" wasm-bindgen = "0.2.91" web-sys = "0.3.68" - -[patch.crates-io] -fn_graph = { path = "/mnt/data/work/github/azriel91/fn_graph" } From c270a0c86b0b64d9f9de86fb001143b27ab9d3f6 Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Thu, 7 Mar 2024 07:39:58 +1300 Subject: [PATCH 24/38] Pass pre-calculated `bucket_name` in params specs in `envman` flows. This avoids issue #188. --- examples/envman/src/flows/app_upload_flow.rs | 6 +++--- examples/envman/src/flows/env_deploy_flow.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/envman/src/flows/app_upload_flow.rs b/examples/envman/src/flows/app_upload_flow.rs index 52d283544..da90b5a65 100644 --- a/examples/envman/src/flows/app_upload_flow.rs +++ b/examples/envman/src/flows/app_upload_flow.rs @@ -11,7 +11,7 @@ use url::Url; use crate::{ items::{ - peace_aws_s3_bucket::{S3BucketItem, S3BucketParams, S3BucketState}, + peace_aws_s3_bucket::{S3BucketItem, S3BucketParams}, peace_aws_s3_object::{S3ObjectItem, S3ObjectParams}, }, model::{EnvManError, RepoSlug, WebApp}, @@ -94,12 +94,12 @@ impl AppUploadFlow { .to_string_lossy() .to_string(); let s3_bucket_params_spec = S3BucketParams::::field_wise_spec() - .with_name(bucket_name) + .with_name(bucket_name.clone()) .build(); let s3_object_params_spec = S3ObjectParams::::field_wise_spec() .with_file_path(web_app_path_local) .with_object_key(object_key) - .with_bucket_name_from_map(S3BucketState::bucket_name) + .with_bucket_name(bucket_name) .build(); Ok(AppUploadFlowParamsSpecs { diff --git a/examples/envman/src/flows/env_deploy_flow.rs b/examples/envman/src/flows/env_deploy_flow.rs index d79f74520..cd936f361 100644 --- a/examples/envman/src/flows/env_deploy_flow.rs +++ b/examples/envman/src/flows/env_deploy_flow.rs @@ -17,7 +17,7 @@ use crate::{ peace_aws_iam_policy::{IamPolicyItem, IamPolicyParams, IamPolicyState}, peace_aws_iam_role::{IamRoleItem, IamRoleParams}, peace_aws_instance_profile::{InstanceProfileItem, InstanceProfileParams}, - peace_aws_s3_bucket::{S3BucketItem, S3BucketParams, S3BucketState}, + peace_aws_s3_bucket::{S3BucketItem, S3BucketParams}, peace_aws_s3_object::{S3ObjectItem, S3ObjectParams}, }, model::{EnvManError, RepoSlug, WebApp}, @@ -151,7 +151,7 @@ impl EnvDeployFlow { .build(); let s3_object_params_spec = S3ObjectParams::::field_wise_spec() .with_file_path(web_app_path_local) - .with_bucket_name_from_map(S3BucketState::bucket_name) + .with_bucket_name(bucket_name) .with_object_key(object_key) .build(); From 54fb00d389ad59ea8fb52a71f251b501518690b9 Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Thu, 7 Mar 2024 07:45:27 +1300 Subject: [PATCH 25/38] Remove unused commented code in `envman`. --- examples/envman/src/main_cli.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/envman/src/main_cli.rs b/examples/envman/src/main_cli.rs index 8d479e319..a79901a12 100644 --- a/examples/envman/src/main_cli.rs +++ b/examples/envman/src/main_cli.rs @@ -136,8 +136,6 @@ async fn run_command( let webi_output = WebiOutput::new(Some(SocketAddr::from((address, port))), flow_spec_info); webi_output.start().await?; - - // WebServer::start(Some(SocketAddr::from((address, port)))).await? } } From 6940a21b0b95ab52414fffe764cf4a29309c075e Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Sat, 9 Mar 2024 16:46:07 +1300 Subject: [PATCH 26/38] Add design docs for `endpoints_and_interaction.md`. --- .typos.toml | 1 + doc/src/SUMMARY.md | 4 + .../endpoints_and_interaction.md | 21 ++ .../cmd_invocation.md | 268 ++++++++++++++++++ .../endpoints_and_interaction/interruption.md | 83 ++++++ .../progress_output.md | 72 +++++ 6 files changed, 449 insertions(+) create mode 100644 doc/src/technical_concepts/endpoints_and_interaction.md create mode 100644 doc/src/technical_concepts/endpoints_and_interaction/cmd_invocation.md create mode 100644 doc/src/technical_concepts/endpoints_and_interaction/interruption.md create mode 100644 doc/src/technical_concepts/endpoints_and_interaction/progress_output.md diff --git a/.typos.toml b/.typos.toml index c58ecb0f3..b29a25359 100644 --- a/.typos.toml +++ b/.typos.toml @@ -6,3 +6,4 @@ extend-exclude = [ ] [default.extend-words] +Lazer = "Lazer" # discord username diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index 606128751..a098fd6e0 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -39,6 +39,10 @@ - [Render Technology](technical_concepts/diagrams/outcome/render_technology.md) - [HTML + Flexbox](technical_concepts/diagrams/outcome/html_flexbox.md) - [Div Diag](technical_concepts/diagrams/outcome/div_diag.md) + - [Endpoints and Interaction](technical_concepts/endpoints_and_interaction.md) + - [Cmd Invocation](technical_concepts/endpoints_and_interaction/cmd_invocation.md) + - [Interruption](technical_concepts/endpoints_and_interaction/interruption.md) + - [Progress Output](technical_concepts/endpoints_and_interaction/progress_output.md) - [Workspace](technical_concepts/workspace.md) - [Commands](technical_concepts/commands.md) - [Scopes](technical_concepts/commands/scopes.md) diff --git a/doc/src/technical_concepts/endpoints_and_interaction.md b/doc/src/technical_concepts/endpoints_and_interaction.md new file mode 100644 index 000000000..19738cd52 --- /dev/null +++ b/doc/src/technical_concepts/endpoints_and_interaction.md @@ -0,0 +1,21 @@ +# Endpoints and Interaction + +## Background + +For a web UI, we want: + +1. A way to invoke `*Cmd`s -- invoke and return an ID. +2. Command invocation request is idempotent / does not initiate another invocation when one is in progress. +3. A way to interrupt `CmdExecution`s -- get the `Sender` by execution ID, send. +4. A way to pull progress from the client -- close / reopen tab, send URL to another person. +5. A way to push progress to the client -- for efficiency. web sockets? +6. For both a local and shared server, a way to open a particular env/`Flow`/`CmdExecution` by default. +7. For a shared server, a way to list `CmdExecution`s. +8. For a local server, to automatically use different ports when running executions in different projects. + + +## Glossary + +| Term | Definition | +|:-----|:-------------------------------------------------------| +| Web | Refers to server side execution, not client side WASM. | diff --git a/doc/src/technical_concepts/endpoints_and_interaction/cmd_invocation.md b/doc/src/technical_concepts/endpoints_and_interaction/cmd_invocation.md new file mode 100644 index 000000000..6d31995fc --- /dev/null +++ b/doc/src/technical_concepts/endpoints_and_interaction/cmd_invocation.md @@ -0,0 +1,268 @@ +# Cmd Invocation + +Need to cater for: + +1. **CLI usage:** Invocation returns the result. +2. **Web usage:** Invocation returns an ID. +3. **Any usage:** For a given profile, two `CmdExecution`s cannot run at the same time, even for different flows. +4. **Web usage:** For a given profile, re-invocation returns existing in-progress `CmdExecution` ID. This can be deferred if two browser tabs for the same workspace + profile combination both disable the deploy button when a `CmdExecution` is initiated. + + +## Option A1: `exec` delegates to `request_exec` + +For the CLI usage, to reduce code duplication `*Cmd`s can provide function that return the `Result`, where internally it calls the method that returns an execution ID, but immediately waits for that execution's completion. + +```rust +pub async fn exec<'ctx>( + cmd_ctx: &mut CmdCtx>, +) -> Result, CmdCtxTypesT::AppError> +where + CmdCtxTypesT: 'ctx, +{ + let execution_id = Self::request_exec(cmd_ctx); + + executions.get(execution_id).await +} +``` + + +## Option A2: `request_exec` delegates to `exec` + +```rust ,ignore +pub async fn request_exec<'ctx>( + cmd_ctx: &mut CmdCtx>, +) -> ExecutionId +where + CmdCtxTypesT: 'ctx, +{ + if let Some(execution_id) = executions.get((workspace, profile)) { + return execution_id; + }; + + let execution_id = server.generate_execution_id(workspace, profile).await; + let cmd_execution = Self::exec(cmd_ctx); + // or send(..) the execution request to a queue, and the queue receiver calls the `exec`. + + executions.put(execution_id, cmd_execution).await + + execution_id +} +``` + +## Web Interface + +Web Server: + +* Needs to hold a collection of all executions. +* Needs to hold mapping from Execution ID to `CmdExecution`, and/or parts of the `CmdExecution`. + + Storing parts separately can with access and extensibility: + + - Sometimes we don't want to borrow the full `CmdExecution`, only part of it. + - Adding new things gets stored in a different server context state, so components that are not concerned with the new state don't need to access it. + + + Need to make sure all context is added in the same place, otherwise it is difficult to track "what makes up a `CmdExecution`". + + +### 1. Web Server `CmdExecutions` Tracking + +* `CmdExecutions` is the collection of in-progress executions, not just their serializable info. + + Possibly a `LinkedHashMap>`, where `CmdExecutionRt` is a trait over the concrete `CmdExecution`s which are type parameterized. + +* `CmdExecutionsInfo` is a serializable collection of both in-progress and historical execution infos. + + Possibly a `LinkedHashMap`. + +```rust ,ignore +// Web Server set up needs to track everything +// or, link to a database that tracks everything +let cmd_executions = CmdExecutions::default(); +let router = Router::new() + // .. + .leptos_routes_with_context( + &leptos_options, + routes, + move || { + // .. + leptos::provide_context(Arc::clone(cmd_executions)); + }, + move || view! { }, + ) + // .. + ; +``` + +### 2. Web Component `CmdExecutionInfos` Access For Display + +`CmdExecutionInfos` is the serializable type used to represent `CmdExecutions` for display: + +```rust ,ignore +/// Returns the list of `CmdExecutions` that have run / are in-progress on the server. +#[leptos::server(endpoint = "/cmd_execution_infos")] +pub async fn cmd_execution_infos( +) -> Result> { + let cmd_execution_infos = leptos::use_context::() + .ok_or_else(|| { + ServerFnError::::ServerError( + "`CmdExecutionInfos` was not set.".to_string() + ) + })?; + + Ok(cmd_execution_infos) +} + +#[component] +pub fn CmdExecutionsList() -> impl IntoView { + let cmd_execution_infos_resource = leptos::create_resource( + || (), + move |()| async move { cmd_execution_infos().await.unwrap() }, + ); + let cmd_execution_infos = move || { + cmd_execution_infos_resource + .get() + .expect("Expected `cmd_execution_infos` to always be generated successfully.") + }; + + view! { + "Loading..."

}> + +
+ } +} +``` + + +### 3. Web Component `CmdExecutions` Access For `*Cmd` Invocation + +Given a `workspace`, `profile`, `flow_id`, a `*Cmd` and `*Cmd` parameters, a user should be able to send a `CmdExecutionRequest`. Some of these parameters should be able to be defaulted, e.g. for a local automation server which is run from the workspace directory. + + +--- + +Should [`create_action`](https://book.leptos.dev/async/13_actions.html) or [`create_server_action`](https://docs.rs/leptos/latest/leptos/fn.create_server_action.html) be used? + +
+ +Answer From `@Lazer` ([discord](https://discord.com/channels/1031524867910148188/1031524868883218474/1215847615183327352)) + +You can provide params to server actions via hidden inputs if necessary. + +```rust ,ignore +#[server(endpoint = "check_code")] +pub async fn check_code(s: Uuid, c: Code) -> Result { + todo!(); +} + +let check_action = create_server_action::(); + + +
+ + +
+ +
+ + + "Didn't get an email?" + +
+ +
+``` + +
+ + +#### Example + +```rust ,ignore +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct EnsureCmdArgs { + workspace: Workspace, + profile: Profile, + flow_id: FlowId, +} + +pub struct CmdExecutionQueues(HashMap<(Workspace, Profile), Sender>); + +#[leptos::server(endpoint = "/ensure_cmd")] +pub async fn ensure_cmd( + ensure_cmd_args: EnsureCmdArgs, +) -> Result> { + let cmd_execution_queues = leptos::use_context::() + .ok_or_else(|| { + ServerFnError::::ServerError( + "`Sender` was not set.".to_string() + ) + })?; + let cmd_execution_infos = leptos::use_context::() + .ok_or_else(|| { + ServerFnError::::ServerError( + "`Sender` was not set.".to_string() + ) + })?; + + let execution_id = cmd_execution_queues.get(&(workspace, profile)) + .map(|cmd_execution_req_tx| { + let execution_id = ExecutionId::new_rand(); + let cmd_execution_req = CmdExecutionReq { + execution_id, + ensure_cmd_args, + }; + + let cmd_execution_info = CmdExecutionInfo::new(execution_id, ensure_cmd_args); + cmd_execution_infos.insert(execution_id, cmd_execution_info); + + cmd_execution_req_tx.send(cmd_execution_req).await; + + execution_id + }) + .ok_or_else(|| { + ServerFnError::::ServerError( + format!("No `CmdExecutionQueue` for {workspace} {profile}.") + ) + }); + + Ok(execution_id) +} + +#[component] +pub fn EnsureButton() -> impl IntoView { + let ensure_cmd = leptos::create_action( + |workspace: Workspace, profile: Profile, flow_id: FlowId| { + let execution_id = execution_id.clone(); + async move { ensure_cmd(EnsureCmdParams { workspace, profile, flow_id }).await } + }, + ); + let submitted = ensure_cmd.input(); // RwSignal> + let pending = ensure_cmd.pending(); // ReadSignal + let todo_id = ensure_cmd.value(); // RwSignal> + + view! { +
+ // Execution ID + +
+ // use our loading state +

{move || pending().then("Loading...")}

+ } +} +``` diff --git a/doc/src/technical_concepts/endpoints_and_interaction/interruption.md b/doc/src/technical_concepts/endpoints_and_interaction/interruption.md new file mode 100644 index 000000000..08de58447 --- /dev/null +++ b/doc/src/technical_concepts/endpoints_and_interaction/interruption.md @@ -0,0 +1,83 @@ +# Interruption + +User has sent an interruption request. These are received differently depending on the `Output` that presented the interface to the user. + +* **CLI:** + + - The developer needs to [define the `SIGINT` handler][sigint_handler_def], and pass it into Peace ([1][interruptibility_augment_add], [2][interruptibility_augment_call]). + - The user presses Ctrl C to interrupt the command execution. + +* **Web:** + + - The framework needs to track an execution ID to the `CmdExecution`'s interrupt sender. + - User sends an interrupt request with the execution ID. + + +## Implementation + +This follows on from the [**Cmd Invocation > Web Interface**](cmd_invocation.md#web-interface) design. + + +### 4. Web Component `InterruptSenders` Access + +`InterruptSenders` may be a newtype for `Map>` + +See: + +* [`leptos`: actions](https://book.leptos.dev/async/13_actions.html). +* [`leptos_router::ActionForm`](https://docs.rs/leptos_router/latest/leptos_router/fn.ActionForm.html) +* [`leptos::create_server_action`](https://docs.rs/leptos/latest/leptos/fn.create_server_action.html) +* [`todo_app_sqlite`](https://github.com/leptos-rs/leptos/blob/main/examples/todo_app_sqlite/src/todo.rs) + + +```rust ,ignore +#[leptos::server(endpoint = "/cmd_exec_interrupt")] +pub async fn cmd_exec_interrupt( + execution_id: &ExecutionId, +) -> Result<(), ServerFnError> { + let interrupt_senders = leptos::use_context::() + .ok_or_else(|| { + ServerFnError::::ServerError( + "`InterruptSenders` was not set.".to_string() + ) + })?; + if let Some(interrupt_sender) = interrupt_senders.get(execution_id) { + interrupt_sender.send(InterruptSignal).await; + } + + Ok(()) +} + +#[component] +pub fn InterruptButton( + execution_id: ReadSignal, +) -> impl IntoView { + let cmd_exec_interrupt_action = leptos::create_action( + |execution_id: &ExecutionId| { + let execution_id = execution_id.clone(); + async move { cmd_exec_interrupt(&execution_id).await } + }, + ); + let submitted = cmd_exec_interrupt_action.input(); // RwSignal> + let pending = cmd_exec_interrupt_action.pending(); // ReadSignal + let todo_id = cmd_exec_interrupt_action.value(); // RwSignal> + + view! { +
+ // Execution ID + +
+ // use our loading state +

{move || pending().then("Loading...")}

+ } +} +``` + +[sigint_handler_def]: https://github.com/azriel91/peace/blob/4e8077103b6361e3e9a58e2adf177df1eec1490b/examples/envman/src/cmds/cmd_ctx_builder.rs#L29-L37 +[interruptibility_augment_add]: https://github.com/azriel91/peace/blob/4e8077103b6361e3e9a58e2adf177df1eec1490b/examples/envman/src/cmds/cmd_ctx_builder.rs#L39-L43 +[interruptibility_augment_call]: https://github.com/azriel91/peace/blob/4e8077103b6361e3e9a58e2adf177df1eec1490b/examples/envman/src/cmds/env_cmd.rs#L68 diff --git a/doc/src/technical_concepts/endpoints_and_interaction/progress_output.md b/doc/src/technical_concepts/endpoints_and_interaction/progress_output.md new file mode 100644 index 000000000..4b9cb587f --- /dev/null +++ b/doc/src/technical_concepts/endpoints_and_interaction/progress_output.md @@ -0,0 +1,72 @@ +# Progress Output + +> See also [Execution Progress](/technical_concepts/output/execution_progress.md). + +The way progress is transferred to the user varies based on the `OutputWrite` and build `target`. + +1. **CLI:** This is pushed straight to the terminal +2. **Web:** This is pulled by the client from the server based on execution ID. +3. **WASM:** This is pushed by the WASM binary within the client. + + +## Implementation + +### Web Interface + +```rust ,ignore +#[component] +pub fn FlowGraph(execution_id: ReadSignal) -> impl IntoView { + let progress_dot_resource = leptos::create_resource( + || (), + move |()| async move { progress_dot_graph(execution_id.get()).await.unwrap() }, + ); + let progress_dot_graph = move || { + let progress_dot_graph = progress_dot_resource + .get() + .expect("Expected `progress_dot_graph` to always be generated successfully."); + + Some(progress_dot_graph) + }; + + view! { +
+ // Execution ID + +
+ // use our loading state +

{move || pending().then("Loading...")}

+ } +} + +/// Returns the graph representing item execution progress. +#[leptos::server(endpoint = "/flow_graph")] +pub async fn progress_dot_graph(execution_id: ExecutionId) -> Result> { + use dot_ix::{ + model::common::{graphviz_dot_theme::GraphStyle, GraphvizDotTheme}, + rt::IntoGraphvizDotSrc, + }; + use peace_flow_model::FlowSpecInfo; + + let flow_spec_info = leptos::use_context::().ok_or_else(|| { + ServerFnError::::ServerError("`FlowSpecInfo` was not set.".to_string()) + })?; + let cmd_progress_trackers = leptos::use_context::().ok_or_else(|| { + ServerFnError::::ServerError("`CmdProgressTrackers` was not set.".to_string()) + })?; + if let Some(cmd_progress_tracker) = cmd_progress_trackers.get(&execution_id) { + // TODO: adjust styles on graph. + } + + let progress_info_graph = flow_spec_info.into_progress_info_graph(); + Ok(IntoGraphvizDotSrc::into( + &progress_info_graph, + &GraphvizDotTheme::default().with_graph_style(GraphStyle::Circle), + )) +} + +``` From d5ac550cf2a0bb85ad05824db8fe241f013f9316 Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Sat, 9 Mar 2024 18:53:52 +1300 Subject: [PATCH 27/38] Install `mdbook-graphviz` from branch. --- .github/workflows/book.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/book.yml b/.github/workflows/book.yml index 1f406749d..d521d2f3c 100644 --- a/.github/workflows/book.yml +++ b/.github/workflows/book.yml @@ -32,7 +32,7 @@ jobs: - name: Setup Graphviz uses: ts-graphviz/setup-graphviz@v1 - - run: cargo install mdbook-graphviz + - run: cargo install mdbook-graphviz --git https://github.com/azriel91/mdbook-graphviz.git --branch maintenance/update-dependencies if: steps.mdbook_graphviz_cache.outputs.cache-hit != 'true' # When updating this, also update ci.yml From e6c26d7a0deae7d3df5276f73fb5415a90c2857f Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Sat, 9 Mar 2024 19:05:42 +1300 Subject: [PATCH 28/38] Build `envman` example with `--bin-features "cli"` in `ci.yml`. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84239b5ad..9a9c5bb38 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -210,7 +210,7 @@ jobs: run: cargo install --git https://github.com/leptos-rs/cargo-leptos.git --locked cargo-leptos - name: 'Example: envman (leptos)' - run: cargo leptos build --project "envman" -v + run: cargo leptos build --project "envman" --bin-features "cli" -v # When updating this, also update book.yml - name: 'Example: download (WASM)' From 8180dcbc5053219aca922b1e28d5f0f1687463f0 Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Sat, 9 Mar 2024 19:06:14 +1300 Subject: [PATCH 29/38] Edit `download` example dependencies to not enable `cli` when compiling to wasm. --- examples/download/Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/download/Cargo.toml b/examples/download/Cargo.toml index fd0658565..10473f22b 100644 --- a/examples/download/Cargo.toml +++ b/examples/download/Cargo.toml @@ -18,16 +18,17 @@ test = false crate-type = ["cdylib", "rlib"] [dependencies] -peace = { path = "../..", default-features = false, features = ["cli"] } peace_items = { path = "../../items", features = ["file_download"] } thiserror = "1.0.57" url = { version = "2.5.0", features = ["serde"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] +peace = { workspace = true, default-features = false, features = ["cli"] } clap = { version = "4.5.1", features = ["derive"] } tokio = { version = "1.36.0", features = ["net", "time", "rt"] } [target.'cfg(target_arch = "wasm32")'.dependencies] +peace = { workspace = true, default-features = false } console_error_panic_hook = "0.1.7" serde-wasm-bindgen = "0.6.4" tokio = "1.36.0" From cbcf918c632385de9633a16ae89945720e31f8c2 Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Sat, 9 Mar 2024 19:32:05 +1300 Subject: [PATCH 30/38] Update dependency versions. --- Cargo.toml | 24 ++++++++++++------------ examples/download/Cargo.toml | 12 ++++++------ examples/envman/Cargo.toml | 36 ++++++++++++++++++------------------ 3 files changed, 36 insertions(+), 36 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3e4a56e5c..5d0167f7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -138,22 +138,22 @@ peace_item_tar_x = { path = "items/tar_x", version = "0.0.13" } # developers to see the dependencies to create an automation tool. async-trait = "0.1.77" axum = "0.7.4" -base64 = "0.21.7" +base64 = "0.22.0" bytes = "1.5.0" cfg-if = "1.0.0" -chrono = { version = "0.4.34", default-features = false, features = ["clock", "serde"] } +chrono = { version = "0.4.35", default-features = false, features = ["clock", "serde"] } console = "0.15.8" derivative = "2.2.0" diff-struct = "0.5.3" downcast-rs = "1.2.0" dot_ix = "0.2.0" -dyn-clone = "1.0.16" +dyn-clone = "1.0.17" enser = "0.1.4" erased-serde = "0.4.3" fn_graph = { version = "0.13.0", features = ["async", "graph_info", "interruptible", "resman"] } futures = "0.3.30" heck = "0.4.1" -indexmap = "2.2.3" +indexmap = "2.2.5" indicatif = "0.17.8" interruptible = "0.2.1" leptos = { version = "0.6" } @@ -161,26 +161,26 @@ leptos_axum = "0.6" leptos_meta = { version = "0.6" } leptos_router = { version = "0.6" } libc = "0.2.153" -miette = "7.1.0" +miette = "7.2.0" pretty_assertions = "1.4.0" proc-macro2 = "1.0.78" quote = "1.0.35" raw_tty = "0.1.0" -reqwest = "0.11.24" +reqwest = "0.11.25" resman = "0.17.0" serde = "1.0.197" -serde-wasm-bindgen = "0.6.4" +serde-wasm-bindgen = "0.6.5" serde_json = "1.0.114" serde_yaml = "0.9.32" -syn = "2.0.50" +syn = "2.0.52" tar = "0.4.40" -tempfile = "3.10.0" +tempfile = "3.10.1" thiserror = "1.0.57" tokio = "1.36" tokio-util = "0.7.10" -tower-http = "0.5.1" +tower-http = "0.5.2" tynm = "0.1.10" type_reg = { version = "0.7.0", features = ["debug", "untagged", "ordered"] } url = "2.5.0" -wasm-bindgen = "0.2.91" -web-sys = "0.3.68" +wasm-bindgen = "0.2.92" +web-sys = "0.3.69" diff --git a/examples/download/Cargo.toml b/examples/download/Cargo.toml index 10473f22b..c05ebd874 100644 --- a/examples/download/Cargo.toml +++ b/examples/download/Cargo.toml @@ -24,18 +24,18 @@ url = { version = "2.5.0", features = ["serde"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] peace = { workspace = true, default-features = false, features = ["cli"] } -clap = { version = "4.5.1", features = ["derive"] } +clap = { version = "4.5.2", features = ["derive"] } tokio = { version = "1.36.0", features = ["net", "time", "rt"] } [target.'cfg(target_arch = "wasm32")'.dependencies] peace = { workspace = true, default-features = false } console_error_panic_hook = "0.1.7" -serde-wasm-bindgen = "0.6.4" +serde-wasm-bindgen = "0.6.5" tokio = "1.36.0" -wasm-bindgen = "0.2.91" -wasm-bindgen-futures = "0.4.41" -js-sys = "0.3.68" -web-sys = "0.3.68" +wasm-bindgen = "0.2.92" +wasm-bindgen-futures = "0.4.42" +js-sys = "0.3.69" +web-sys = "0.3.69" [features] default = [] diff --git a/examples/envman/Cargo.toml b/examples/envman/Cargo.toml index 5842f3f06..1d95706ea 100644 --- a/examples/envman/Cargo.toml +++ b/examples/envman/Cargo.toml @@ -18,13 +18,13 @@ test = false crate-type = ["cdylib", "rlib"] [dependencies] -aws-config = { version = "1.1.6", optional = true } -aws-sdk-iam = { version = "1.14.0", optional = true } -aws-sdk-s3 = { version = "1.16.0", optional = true } +aws-config = { version = "1.1.7", optional = true } +aws-sdk-iam = { version = "1.15.0", optional = true } +aws-sdk-s3 = { version = "1.17.0", optional = true } aws-smithy-types = { version = "1.1.7", optional = true } # used to reference error type, otherwise not recommended for direct usage -base64 = { version = "0.21.7", optional = true } +base64 = { version = "0.22.0", optional = true } cfg-if = "1.0.0" -chrono = { version = "0.4.34", default-features = false, features = ["clock", "serde"], optional = true } +chrono = { version = "0.4.35", default-features = false, features = ["clock", "serde"], optional = true } derivative = { version = "2.2.0", optional = true } futures = { version = "0.3.30", optional = true } md5-rs = { version = "0.1.5", optional = true } # WASM compatible, and reads bytes as stream @@ -35,34 +35,34 @@ serde = { version = "1.0.197", features = ["derive"] } thiserror = { version = "1.0.57", optional = true } url = { version = "2.5.0", features = ["serde"] } urlencoding = { version = "2.1.3", optional = true } -whoami = { version = "1.4.1", optional = true } +whoami = { version = "1.5.0", optional = true } # web_server # ssr axum = { version = "0.7.4", optional = true } hyper = { version = "1.2.0", optional = true } -leptos = { version = "0.6.6", default-features = false, features = ["serde"] } -leptos_axum = { version = "0.6.6", optional = true } -leptos_meta = { version = "0.6.6", default-features = false } -leptos_router = { version = "0.6.6", default-features = false } +leptos = { version = "0.6.9", default-features = false, features = ["serde"] } +leptos_axum = { version = "0.6.9", optional = true } +leptos_meta = { version = "0.6.9", default-features = false } +leptos_router = { version = "0.6.9", default-features = false } tower = { version = "0.4.13", optional = true } -tower-http = { version = "0.5.1", optional = true, features = ["fs"] } +tower-http = { version = "0.5.2", optional = true, features = ["fs"] } tracing = { version = "0.1.40", optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -clap = { version = "4.5.1", features = ["derive"], optional = true } +clap = { version = "4.5.2", features = ["derive"], optional = true } tokio = { version = "1.36.0", features = ["rt", "rt-multi-thread", "signal"], optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] console_error_panic_hook = "0.1.7" console_log = { version = "1.0.0", features = ["color"] } -log = "0.4.20" -serde-wasm-bindgen = "0.6.4" +log = "0.4.21" +serde-wasm-bindgen = "0.6.5" tokio = "1.36.0" -wasm-bindgen = "0.2.91" -wasm-bindgen-futures = "0.4.41" -js-sys = "0.3.68" -web-sys = "0.3.68" +wasm-bindgen = "0.2.92" +wasm-bindgen-futures = "0.4.42" +js-sys = "0.3.69" +web-sys = "0.3.69" [features] default = [] From 062f6ad27a7846f6464609d5817ec11314319697 Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Sat, 9 Mar 2024 19:33:06 +1300 Subject: [PATCH 31/38] Adjust `miette` features to allow WASM compilation. --- Cargo.toml | 7 ++++++- crate/cmd_model/Cargo.toml | 2 +- crate/cmd_rt/Cargo.toml | 2 +- crate/rt_model_core/Cargo.toml | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5d0167f7b..1b8bc5798 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,12 @@ peace_webi = { workspace = true, optional = true } peace_webi_components = { workspace = true, optional = true } peace_webi_model = { workspace = true, optional = true } +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +miette = { workspace = true, optional = true, features = ["fancy"] } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +miette = { workspace = true, optional = true, features = ["fancy-no-syscall"] } + [features] default = [] cli = [ @@ -50,7 +56,6 @@ webi = [ ] error_reporting = [ "dep:miette", - "miette?/fancy", "peace_cmd_model/error_reporting", "peace_cmd_rt/error_reporting", "peace_params/error_reporting", diff --git a/crate/cmd_model/Cargo.toml b/crate/cmd_model/Cargo.toml index a6f44e234..08436dfef 100644 --- a/crate/cmd_model/Cargo.toml +++ b/crate/cmd_model/Cargo.toml @@ -19,7 +19,7 @@ test = false [dependencies] fn_graph = { workspace = true } futures = { workspace = true } -miette = { workspace = true, optional = true, features = ["fancy"] } +miette = { workspace = true, optional = true } indexmap = { workspace = true } peace_cfg = { workspace = true } thiserror = { workspace = true } diff --git a/crate/cmd_rt/Cargo.toml b/crate/cmd_rt/Cargo.toml index ec788c5b0..4c914911c 100644 --- a/crate/cmd_rt/Cargo.toml +++ b/crate/cmd_rt/Cargo.toml @@ -23,7 +23,7 @@ fn_graph = { workspace = true } futures = { workspace = true } indexmap = { workspace = true } interruptible = { workspace = true } -miette = { workspace = true, optional = true, features = ["fancy"] } +miette = { workspace = true, optional = true } peace_cfg = { workspace = true } peace_cmd_model = { workspace = true } peace_cmd = { workspace = true } diff --git a/crate/rt_model_core/Cargo.toml b/crate/rt_model_core/Cargo.toml index 681094e59..785882369 100644 --- a/crate/rt_model_core/Cargo.toml +++ b/crate/rt_model_core/Cargo.toml @@ -21,7 +21,7 @@ async-trait = { workspace = true } cfg-if = { workspace = true } indicatif = { workspace = true, features = ["tokio"] } indexmap = { workspace = true } -miette = { workspace = true, optional = true, features = ["fancy"] } +miette = { workspace = true, optional = true } peace_core = { workspace = true } peace_cmd_model = { workspace = true } peace_fmt = { workspace = true } From 7465df50ebbd56caa6f19ca8d16534ca82a9b1f6 Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Sat, 9 Mar 2024 19:34:30 +1300 Subject: [PATCH 32/38] Address clippy lints. --- crate/flow_model/src/flow_spec_info.rs | 6 +++--- crate/webi_output/src/webi_output.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crate/flow_model/src/flow_spec_info.rs b/crate/flow_model/src/flow_spec_info.rs index 13fddf4d3..448c9fa29 100644 --- a/crate/flow_model/src/flow_spec_info.rs +++ b/crate/flow_model/src/flow_spec_info.rs @@ -100,7 +100,7 @@ fn outcome_node_hierarchy( let mut hierarchy = NodeHierarchy::new(); let children = graph_info.children(node_index); children - .iter(&*graph_info) + .iter(graph_info) .filter_map(|(edge_index, child_node_index)| { // For outcome graphs, child nodes that: // @@ -138,7 +138,7 @@ fn outcome_node_edges(graph_info: &GraphInfo) -> IndexMap) -> IndexMap }); - stream::iter(crate::assets::ASSETS.into_iter()) + stream::iter(crate::assets::ASSETS.iter()) .map(Result::<_, WebiError>::Ok) .try_for_each(|(path_str, contents)| async move { let asset_path = Path::new(path_str); From 224b34df4a4187b026e1ea29103a2581a1fa4e15 Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Sat, 9 Mar 2024 19:36:58 +1300 Subject: [PATCH 33/38] Update `S3BucketStateCurrentFn` to use non-deprecated `DateTime` method. --- .../s3_bucket_state_current_fn.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/examples/envman/src/items/peace_aws_s3_bucket/s3_bucket_state_current_fn.rs b/examples/envman/src/items/peace_aws_s3_bucket/s3_bucket_state_current_fn.rs index e7467b6b1..971fb6e92 100644 --- a/examples/envman/src/items/peace_aws_s3_bucket/s3_bucket_state_current_fn.rs +++ b/examples/envman/src/items/peace_aws_s3_bucket/s3_bucket_state_current_fn.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use chrono::{DateTime, NaiveDateTime, Utc}; +use chrono::DateTime; use peace::{ cfg::{state::Timestamped, FnCtx}, params::Params, @@ -128,14 +128,10 @@ where if let Some(creation_date) = creation_date { let state_current = S3BucketState::Some { name: name.to_string(), - creation_date: Timestamped::Value(DateTime::from_naive_utc_and_offset( - NaiveDateTime::from_timestamp_opt( - creation_date.secs(), - creation_date.subsec_nanos(), - ) - .unwrap(), - Utc, - )), + creation_date: Timestamped::Value( + DateTime::from_timestamp(creation_date.secs(), creation_date.subsec_nanos()) + .unwrap(), + ), }; Ok(state_current) From 12fffbb0dd0483d6d95c3fbb10c05dd69b5e3b4e Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Sat, 9 Mar 2024 21:34:27 +1300 Subject: [PATCH 34/38] Use `actions/checkout@v4` in github actions. --- .github/workflows/book.yml | 2 +- .github/workflows/ci.yml | 20 ++++++++++---------- .github/workflows/publish.yml | 8 ++++---- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/book.yml b/.github/workflows/book.yml index d521d2f3c..dd54f82b7 100644 --- a/.github/workflows/book.yml +++ b/.github/workflows/book.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - name: 'Install `wasm-pack`' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a9c5bb38..514909722 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: bp3d-actions/audit-check@9c23bd47e5e7b15b824739e0862cb878a52cc211 with: token: ${{ secrets.GITHUB_TOKEN }} @@ -23,7 +23,7 @@ jobs: name: Licenses runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: EmbarkStudios/cargo-deny-action@v1 - name: cargo-about cache @@ -45,7 +45,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: toolchain: nightly @@ -61,7 +61,7 @@ jobs: RUSTDOCFLAGS: "-Dwarnings" steps: - name: Checkout Actions Repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - name: Check spelling @@ -74,7 +74,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: toolchain: nightly @@ -120,7 +120,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: toolchain: nightly @@ -145,7 +145,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: taiki-e/install-action@nextest @@ -161,7 +161,7 @@ jobs: - name: Prepare symlink configuration run: git config --global core.symlinks true - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: taiki-e/install-action@nextest @@ -173,7 +173,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - name: 'Example: download (native)' @@ -187,7 +187,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: toolchain: stable diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d74e612d4..9b817ebea 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: bp3d-actions/audit-check@9c23bd47e5e7b15b824739e0862cb878a52cc211 with: token: ${{ secrets.GITHUB_TOKEN }} @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: taiki-e/install-action@nextest @@ -36,7 +36,7 @@ jobs: - name: Prepare symlink configuration run: git config --global core.symlinks true - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: taiki-e/install-action@nextest @@ -53,7 +53,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 25 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - name: cargo-release Cache From 70f636816b1baf75cf1478b8f19e48c8904facb1 Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Sat, 9 Mar 2024 21:32:59 +1300 Subject: [PATCH 35/38] Remove debuginfo in coverage job. --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 514909722..e0a7c9c49 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -129,6 +129,9 @@ jobs: - uses: taiki-e/install-action@cargo-llvm-cov - uses: taiki-e/install-action@nextest + - name: 'Configure build to remove debuginfo' + run: echo $'\n[profile.dev]\ndebug = false' >> Cargo.toml + - name: 'Collect coverage' run: ./coverage.sh From f80cc25e192a31f14860e552d9e1d813be8b0854 Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Sun, 10 Mar 2024 18:42:38 +1300 Subject: [PATCH 36/38] Add tests for `flow_model` types. --- Cargo.toml | 2 +- crate/flow_model/src/flow_info.rs | 10 + crate/flow_model/src/flow_spec_info.rs | 8 + crate/flow_model/src/item_info.rs | 7 + crate/flow_model/src/item_spec_info.rs | 7 + crate/flow_model/src/lib.rs | 3 + crate/rt_model/src/flow.rs | 5 +- workspace_tests/src/flow_model.rs | 4 + workspace_tests/src/flow_model/flow_info.rs | 178 ++++++++++++++++++ .../src/flow_model/flow_spec_info.rs | 154 +++++++++++++++ workspace_tests/src/flow_model/item_info.rs | 34 ++++ .../src/flow_model/item_spec_info.rs | 37 ++++ workspace_tests/src/lib.rs | 1 + 13 files changed, 445 insertions(+), 5 deletions(-) create mode 100644 workspace_tests/src/flow_model.rs create mode 100644 workspace_tests/src/flow_model/flow_info.rs create mode 100644 workspace_tests/src/flow_model/flow_spec_info.rs create mode 100644 workspace_tests/src/flow_model/item_info.rs create mode 100644 workspace_tests/src/flow_model/item_spec_info.rs diff --git a/Cargo.toml b/Cargo.toml index 1b8bc5798..1d190c787 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -155,7 +155,7 @@ dot_ix = "0.2.0" dyn-clone = "1.0.17" enser = "0.1.4" erased-serde = "0.4.3" -fn_graph = { version = "0.13.0", features = ["async", "graph_info", "interruptible", "resman"] } +fn_graph = { version = "0.13.2", features = ["async", "graph_info", "interruptible", "resman"] } futures = "0.3.30" heck = "0.4.1" indexmap = "2.2.5" diff --git a/crate/flow_model/src/flow_info.rs b/crate/flow_model/src/flow_info.rs index fee60679b..7ae0728cb 100644 --- a/crate/flow_model/src/flow_info.rs +++ b/crate/flow_model/src/flow_info.rs @@ -17,3 +17,13 @@ pub struct FlowInfo { /// Serialized representation of the flow graph. pub graph_info: GraphInfo, } + +impl FlowInfo { + /// Returns a new `FlowInfo`. + pub fn new(flow_id: FlowId, graph_info: GraphInfo) -> Self { + Self { + flow_id, + graph_info, + } + } +} diff --git a/crate/flow_model/src/flow_spec_info.rs b/crate/flow_model/src/flow_spec_info.rs index 448c9fa29..6303670ec 100644 --- a/crate/flow_model/src/flow_spec_info.rs +++ b/crate/flow_model/src/flow_spec_info.rs @@ -23,6 +23,14 @@ pub struct FlowSpecInfo { } impl FlowSpecInfo { + /// Returns a new `FlowSpecInfo`. + pub fn new(flow_id: FlowId, graph_info: GraphInfo) -> Self { + Self { + flow_id, + graph_info, + } + } + /// Returns an [`InfoGraph`] that represents the progress of the flow's /// execution. pub fn into_progress_info_graph(&self) -> InfoGraph { diff --git a/crate/flow_model/src/item_info.rs b/crate/flow_model/src/item_info.rs index 6b1656615..29882e5dc 100644 --- a/crate/flow_model/src/item_info.rs +++ b/crate/flow_model/src/item_info.rs @@ -9,3 +9,10 @@ pub struct ItemInfo { /// ID of the `Item`. pub item_id: ItemId, } + +impl ItemInfo { + /// Returns a new `ItemInfo`. + pub fn new(item_id: ItemId) -> Self { + Self { item_id } + } +} diff --git a/crate/flow_model/src/item_spec_info.rs b/crate/flow_model/src/item_spec_info.rs index a95f948f0..e5ff66e1a 100644 --- a/crate/flow_model/src/item_spec_info.rs +++ b/crate/flow_model/src/item_spec_info.rs @@ -9,3 +9,10 @@ pub struct ItemSpecInfo { /// ID of the `Item`. pub item_id: ItemId, } + +impl ItemSpecInfo { + /// Returns a new `ItemSpecInfo`. + pub fn new(item_id: ItemId) -> Self { + Self { item_id } + } +} diff --git a/crate/flow_model/src/lib.rs b/crate/flow_model/src/lib.rs index e8822b35f..f2d782848 100644 --- a/crate/flow_model/src/lib.rs +++ b/crate/flow_model/src/lib.rs @@ -3,6 +3,9 @@ //! This includes the serializable representation of a `Flow`. Since an actual //! `Flow` contains logic, it currently resides in `peace_rt_model`. +// Re-exports; +pub use fn_graph::GraphInfo; + pub use crate::{ flow_info::FlowInfo, flow_spec_info::FlowSpecInfo, item_info::ItemInfo, item_spec_info::ItemSpecInfo, diff --git a/crate/rt_model/src/flow.rs b/crate/rt_model/src/flow.rs index 2780f7258..9a804b64c 100644 --- a/crate/rt_model/src/flow.rs +++ b/crate/rt_model/src/flow.rs @@ -72,9 +72,6 @@ impl Flow { ItemSpecInfo { item_id } }); - FlowSpecInfo { - flow_id, - graph_info, - } + FlowSpecInfo::new(flow_id, graph_info) } } diff --git a/workspace_tests/src/flow_model.rs b/workspace_tests/src/flow_model.rs new file mode 100644 index 000000000..d6e8de6ba --- /dev/null +++ b/workspace_tests/src/flow_model.rs @@ -0,0 +1,4 @@ +mod flow_info; +mod flow_spec_info; +mod item_info; +mod item_spec_info; diff --git a/workspace_tests/src/flow_model/flow_info.rs b/workspace_tests/src/flow_model/flow_info.rs new file mode 100644 index 000000000..7520d5765 --- /dev/null +++ b/workspace_tests/src/flow_model/flow_info.rs @@ -0,0 +1,178 @@ +use peace::{ + cfg::{flow_id, item_id}, + data::fn_graph::{daggy::Dag, Edge, WouldCycle}, + flow_model::{FlowInfo, FlowSpecInfo, GraphInfo, ItemInfo, ItemSpecInfo}, + rt_model::{Flow, ItemGraph, ItemGraphBuilder}, +}; +use peace_items::blank::BlankItem; + +use crate::PeaceTestError; + +#[test] +fn clone() -> Result<(), Box> { + let flow_info = flow_info()?; + + assert_eq!(flow_info, Clone::clone(&flow_info)); + Ok(()) +} + +#[test] +fn debug() -> Result<(), Box> { + let flow_info = flow_info()?; + + assert_eq!( + "FlowInfo { \ + flow_id: FlowId(\"flow_id\"), \ + graph_info: GraphInfo { \ + graph: Dag { graph: Graph { Ty: \"Directed\", node_count: 6, edge_count: 9, edges: (0, 1), (0, 2), (1, 4), (2, 3), (3, 4), (5, 4), (1, 2), (5, 1), (0, 5), node weights: {0: ItemInfo { item_id: ItemId(\"a\") }, 1: ItemInfo { item_id: ItemId(\"b\") }, 2: ItemInfo { item_id: ItemId(\"c\") }, 3: ItemInfo { item_id: ItemId(\"d\") }, 4: ItemInfo { item_id: ItemId(\"e\") }, 5: ItemInfo { item_id: ItemId(\"f\") }}, edge weights: {0: Logic, 1: Logic, 2: Logic, 3: Logic, 4: Logic, 5: Logic, 6: Data, 7: Data, 8: Data} }, cycle_state: DfsSpace { dfs: Dfs { stack: [], discovered: FixedBitSet { data: [], length: 0 } } } } \ + } \ + }", + format!("{flow_info:?}") + ); + Ok(()) +} + +#[test] +fn serialize() -> Result<(), Box> { + let flow_info = flow_info()?; + + assert_eq!( + r#"flow_id: flow_id +graph_info: + graph: + nodes: + - item_id: a + - item_id: b + - item_id: c + - item_id: d + - item_id: e + - item_id: f + node_holes: [] + edge_property: directed + edges: + - - 0 + - 1 + - Logic + - - 0 + - 2 + - Logic + - - 1 + - 4 + - Logic + - - 2 + - 3 + - Logic + - - 3 + - 4 + - Logic + - - 5 + - 4 + - Logic + - - 1 + - 2 + - Data + - - 5 + - 1 + - Data + - - 0 + - 5 + - Data +"#, + serde_yaml::to_string(&flow_info)? + ); + Ok(()) +} + +#[test] +fn deserialize() -> Result<(), Box> { + let flow_info = flow_info()?; + + assert_eq!( + flow_info, + serde_yaml::from_str( + r#"flow_id: flow_id +graph_info: + graph: + nodes: + - item_id: a + - item_id: b + - item_id: c + - item_id: d + - item_id: e + - item_id: f + node_holes: [] + edge_property: directed + edges: + - [0, 1, Logic] + - [0, 2, Logic] + - [1, 4, Logic] + - [2, 3, Logic] + - [3, 4, Logic] + - [5, 4, Logic] + - [1, 2, Data] + - [5, 1, Data] + - [0, 5, Data] +"# + )? + ); + Ok(()) +} + +fn flow_info() -> Result> { + let flow = Flow::new(flow_id!("flow_id"), complex_graph()?); + let FlowSpecInfo { + flow_id, + graph_info, + } = flow.flow_spec_info(); + + let mut graph = graph_info.iter_insertion_with_indices().fold( + Dag::new(), + |mut graph, (_, item_spec_info)| { + let ItemSpecInfo { item_id } = item_spec_info; + let item_info = ItemInfo::new(item_id.clone()); + graph.add_node(item_info); + graph + }, + ); + + let edges = graph_info + .raw_edges() + .iter() + .map(|e| (e.source(), e.target(), e.weight)); + + graph.add_edges(edges).expect( + "Edges are all directed from the original graph, \ + so this cannot cause a cycle.", + ); + + let graph_info = GraphInfo::new(graph); + let flow_info = FlowInfo::new(flow_id, graph_info); + Ok(flow_info) +} + +fn complex_graph() -> Result, WouldCycle> { + // a - b --------- e + // \ / / + // '-- c - d / + // / + // f --------' + let mut item_graph_builder = ItemGraphBuilder::new(); + let [fn_id_a, fn_id_b, fn_id_c, fn_id_d, fn_id_e, fn_id_f] = item_graph_builder.add_fns([ + BlankItem::<()>::new(item_id!("a")).into(), + BlankItem::<()>::new(item_id!("b")).into(), + BlankItem::<()>::new(item_id!("c")).into(), + BlankItem::<()>::new(item_id!("d")).into(), + BlankItem::<()>::new(item_id!("e")).into(), + BlankItem::<()>::new(item_id!("f")).into(), + ]); + item_graph_builder.add_logic_edges([ + (fn_id_a, fn_id_b), + (fn_id_a, fn_id_c), + (fn_id_b, fn_id_e), + (fn_id_c, fn_id_d), + (fn_id_d, fn_id_e), + (fn_id_f, fn_id_e), + ])?; + let item_graph = item_graph_builder.build(); + Ok(item_graph) +} diff --git a/workspace_tests/src/flow_model/flow_spec_info.rs b/workspace_tests/src/flow_model/flow_spec_info.rs new file mode 100644 index 000000000..d0db0fbab --- /dev/null +++ b/workspace_tests/src/flow_model/flow_spec_info.rs @@ -0,0 +1,154 @@ +use peace::{ + cfg::{flow_id, item_id}, + data::fn_graph::{Edge, WouldCycle}, + flow_model::FlowSpecInfo, + rt_model::{Flow, ItemGraph, ItemGraphBuilder}, +}; +use peace_items::blank::BlankItem; + +use crate::PeaceTestError; + +#[test] +fn clone() -> Result<(), Box> { + let flow_spec_info = flow_spec_info()?; + + assert_eq!(flow_spec_info, Clone::clone(&flow_spec_info)); + Ok(()) +} + +#[test] +fn debug() -> Result<(), Box> { + let flow_spec_info = flow_spec_info()?; + + assert_eq!( + "FlowSpecInfo { \ + flow_id: FlowId(\"flow_id\"), \ + graph_info: GraphInfo { \ + graph: Dag { graph: Graph { Ty: \"Directed\", node_count: 6, edge_count: 9, edges: (0, 1), (0, 2), (1, 4), (2, 3), (3, 4), (5, 4), (1, 2), (5, 1), (0, 5), node weights: {0: ItemSpecInfo { item_id: ItemId(\"a\") }, 1: ItemSpecInfo { item_id: ItemId(\"b\") }, 2: ItemSpecInfo { item_id: ItemId(\"c\") }, 3: ItemSpecInfo { item_id: ItemId(\"d\") }, 4: ItemSpecInfo { item_id: ItemId(\"e\") }, 5: ItemSpecInfo { item_id: ItemId(\"f\") }}, edge weights: {0: Logic, 1: Logic, 2: Logic, 3: Logic, 4: Logic, 5: Logic, 6: Data, 7: Data, 8: Data} }, cycle_state: DfsSpace { dfs: Dfs { stack: [], discovered: FixedBitSet { data: [], length: 0 } } } } \ + } \ + }", + format!("{flow_spec_info:?}") + ); + Ok(()) +} + +#[test] +fn serialize() -> Result<(), Box> { + let flow_spec_info = flow_spec_info()?; + + assert_eq!( + r#"flow_id: flow_id +graph_info: + graph: + nodes: + - item_id: a + - item_id: b + - item_id: c + - item_id: d + - item_id: e + - item_id: f + node_holes: [] + edge_property: directed + edges: + - - 0 + - 1 + - Logic + - - 0 + - 2 + - Logic + - - 1 + - 4 + - Logic + - - 2 + - 3 + - Logic + - - 3 + - 4 + - Logic + - - 5 + - 4 + - Logic + - - 1 + - 2 + - Data + - - 5 + - 1 + - Data + - - 0 + - 5 + - Data +"#, + serde_yaml::to_string(&flow_spec_info)? + ); + Ok(()) +} + +#[test] +fn deserialize() -> Result<(), Box> { + let flow_spec_info = flow_spec_info()?; + + assert_eq!( + flow_spec_info, + serde_yaml::from_str( + r#"flow_id: flow_id +graph_info: + graph: + nodes: + - item_id: a + - item_id: b + - item_id: c + - item_id: d + - item_id: e + - item_id: f + node_holes: [] + edge_property: directed + edges: + - [0, 1, Logic] + - [0, 2, Logic] + - [1, 4, Logic] + - [2, 3, Logic] + - [3, 4, Logic] + - [5, 4, Logic] + - [1, 2, Data] + - [5, 1, Data] + - [0, 5, Data] +"# + )? + ); + Ok(()) +} + +fn flow_spec_info() -> Result> { + let flow_spec_info: FlowSpecInfo = { + let flow = Flow::new(flow_id!("flow_id"), complex_graph()?); + flow.flow_spec_info() + }; + Ok(flow_spec_info) +} + +fn complex_graph() -> Result, WouldCycle> { + // a - b --------- e + // \ / / + // '-- c - d / + // / + // f --------' + let mut item_graph_builder = ItemGraphBuilder::new(); + let [fn_id_a, fn_id_b, fn_id_c, fn_id_d, fn_id_e, fn_id_f] = item_graph_builder.add_fns([ + BlankItem::<()>::new(item_id!("a")).into(), + BlankItem::<()>::new(item_id!("b")).into(), + BlankItem::<()>::new(item_id!("c")).into(), + BlankItem::<()>::new(item_id!("d")).into(), + BlankItem::<()>::new(item_id!("e")).into(), + BlankItem::<()>::new(item_id!("f")).into(), + ]); + item_graph_builder.add_logic_edges([ + (fn_id_a, fn_id_b), + (fn_id_a, fn_id_c), + (fn_id_b, fn_id_e), + (fn_id_c, fn_id_d), + (fn_id_d, fn_id_e), + (fn_id_f, fn_id_e), + ])?; + let item_graph = item_graph_builder.build(); + Ok(item_graph) +} diff --git a/workspace_tests/src/flow_model/item_info.rs b/workspace_tests/src/flow_model/item_info.rs new file mode 100644 index 000000000..593e3cce8 --- /dev/null +++ b/workspace_tests/src/flow_model/item_info.rs @@ -0,0 +1,34 @@ +use peace::{cfg::item_id, flow_model::ItemInfo}; + +#[test] +fn clone() { + let item_info = ItemInfo::new(item_id!("item_id")); + + assert_eq!(item_info, Clone::clone(&item_info)); +} + +#[test] +fn debug() { + let item_info = ItemInfo::new(item_id!("item_id")); + + assert_eq!( + "ItemInfo { item_id: ItemId(\"item_id\") }", + format!("{item_info:?}") + ); +} + +#[test] +fn serialize() -> Result<(), serde_yaml::Error> { + let item_info = ItemInfo::new(item_id!("item_id")); + + assert_eq!("item_id: item_id\n", serde_yaml::to_string(&item_info)?); + Ok(()) +} + +#[test] +fn deserialize() -> Result<(), serde_yaml::Error> { + let item_info = ItemInfo::new(item_id!("item_id")); + + assert_eq!(item_info, serde_yaml::from_str("item_id: item_id\n")?); + Ok(()) +} diff --git a/workspace_tests/src/flow_model/item_spec_info.rs b/workspace_tests/src/flow_model/item_spec_info.rs new file mode 100644 index 000000000..b056c4c9c --- /dev/null +++ b/workspace_tests/src/flow_model/item_spec_info.rs @@ -0,0 +1,37 @@ +use peace::{cfg::item_id, flow_model::ItemSpecInfo}; + +#[test] +fn clone() { + let item_spec_info = ItemSpecInfo::new(item_id!("item_id")); + + assert_eq!(item_spec_info, Clone::clone(&item_spec_info)); +} + +#[test] +fn debug() { + let item_spec_info = ItemSpecInfo::new(item_id!("item_id")); + + assert_eq!( + "ItemSpecInfo { item_id: ItemId(\"item_id\") }", + format!("{item_spec_info:?}") + ); +} + +#[test] +fn serialize() -> Result<(), serde_yaml::Error> { + let item_spec_info = ItemSpecInfo::new(item_id!("item_id")); + + assert_eq!( + "item_id: item_id\n", + serde_yaml::to_string(&item_spec_info)? + ); + Ok(()) +} + +#[test] +fn deserialize() -> Result<(), serde_yaml::Error> { + let item_spec_info = ItemSpecInfo::new(item_id!("item_id")); + + assert_eq!(item_spec_info, serde_yaml::from_str("item_id: item_id\n")?); + Ok(()) +} diff --git a/workspace_tests/src/lib.rs b/workspace_tests/src/lib.rs index 5d961cda2..fd41ffbd7 100644 --- a/workspace_tests/src/lib.rs +++ b/workspace_tests/src/lib.rs @@ -23,6 +23,7 @@ mod cmd_model; mod cmd_rt; mod data; mod diff; +mod flow_model; mod fmt; mod params; mod resources; From f54c0f255fd443a5292669983597fab38e51f0bb Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Fri, 15 Mar 2024 18:49:33 +1300 Subject: [PATCH 37/38] Add tests for `FlowSpecInfo::to_*_info_graph`. --- Cargo.toml | 2 +- crate/flow_model/Cargo.toml | 2 +- crate/flow_model/src/flow_spec_info.rs | 8 +- crate/flow_model/src/lib.rs | 1 + crate/webi_components/Cargo.toml | 2 +- crate/webi_components/src/flow_graph.rs | 4 +- .../src/flow_model/flow_spec_info.rs | 144 ++++++++++++++++-- 7 files changed, 140 insertions(+), 23 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1d190c787..f9032c58e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -151,7 +151,7 @@ console = "0.15.8" derivative = "2.2.0" diff-struct = "0.5.3" downcast-rs = "1.2.0" -dot_ix = "0.2.0" +dot_ix = { version = "0.4.1", default-features = false } dyn-clone = "1.0.17" enser = "0.1.4" erased-serde = "0.4.3" diff --git a/crate/flow_model/Cargo.toml b/crate/flow_model/Cargo.toml index 045d888c0..4fff594b5 100644 --- a/crate/flow_model/Cargo.toml +++ b/crate/flow_model/Cargo.toml @@ -13,7 +13,7 @@ keywords.workspace = true license.workspace = true [dependencies] -dot_ix = { workspace = true } +dot_ix = { workspace = true, default-features = false } fn_graph = { workspace = true, features = ["graph_info"] } peace_core = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/crate/flow_model/src/flow_spec_info.rs b/crate/flow_model/src/flow_spec_info.rs index 6303670ec..f05496c65 100644 --- a/crate/flow_model/src/flow_spec_info.rs +++ b/crate/flow_model/src/flow_spec_info.rs @@ -33,7 +33,7 @@ impl FlowSpecInfo { /// Returns an [`InfoGraph`] that represents the progress of the flow's /// execution. - pub fn into_progress_info_graph(&self) -> InfoGraph { + pub fn to_progress_info_graph(&self) -> InfoGraph { let graph_info = &self.graph_info; let item_count = graph_info.node_count(); @@ -60,7 +60,7 @@ impl FlowSpecInfo { /// Returns an [`InfoGraph`] that represents the outcome of the flow's /// execution. - pub fn into_outcome_info_graph(&self) -> InfoGraph { + pub fn to_outcome_info_graph(&self) -> InfoGraph { let graph_info = &self.graph_info; let item_count = graph_info.node_count(); @@ -165,7 +165,7 @@ fn outcome_node_edges(graph_info: &GraphInfo) -> IndexMap) -> IndexMap Result::ServerError("`FlowSpecInfo` was not set.".to_string()) })?; - let progress_info_graph = flow_spec_info.into_progress_info_graph(); + let progress_info_graph = flow_spec_info.to_progress_info_graph(); Ok(IntoGraphvizDotSrc::into( &progress_info_graph, &GraphvizDotTheme::default().with_graph_style(GraphStyle::Circle), @@ -74,7 +74,7 @@ pub async fn outcome_dot_graph() -> Result::ServerError("`FlowSpecInfo` was not set.".to_string()) })?; - let outcome_info_graph = flow_spec_info.into_outcome_info_graph(); + let outcome_info_graph = flow_spec_info.to_outcome_info_graph(); Ok(IntoGraphvizDotSrc::into( &outcome_info_graph, &GraphvizDotTheme::default().with_graph_style(GraphStyle::Boxes), diff --git a/workspace_tests/src/flow_model/flow_spec_info.rs b/workspace_tests/src/flow_model/flow_spec_info.rs index d0db0fbab..5a25a78bd 100644 --- a/workspace_tests/src/flow_model/flow_spec_info.rs +++ b/workspace_tests/src/flow_model/flow_spec_info.rs @@ -1,13 +1,114 @@ use peace::{ cfg::{flow_id, item_id}, data::fn_graph::{Edge, WouldCycle}, - flow_model::FlowSpecInfo, + flow_model::{ + dot_ix::{ + self, + model::{ + common::{EdgeId, NodeHierarchy, NodeId}, + edge_id, + info_graph::{GraphDir, InfoGraph, NodeInfo}, + node_id, IndexMap, + }, + }, + FlowSpecInfo, + }, rt_model::{Flow, ItemGraph, ItemGraphBuilder}, }; use peace_items::blank::BlankItem; use crate::PeaceTestError; +#[test] +fn to_progress_info_graph() -> Result<(), Box> { + let flow_spec_info = flow_spec_info()?; + + let info_graph = flow_spec_info.to_progress_info_graph(); + + let info_graph_expected = { + let mut node_hierarchy = NodeHierarchy::new(); + node_hierarchy.insert(node_id!("a"), NodeHierarchy::new()); + node_hierarchy.insert(node_id!("b"), NodeHierarchy::new()); + node_hierarchy.insert(node_id!("c"), NodeHierarchy::new()); + node_hierarchy.insert(node_id!("d"), NodeHierarchy::new()); + node_hierarchy.insert(node_id!("e"), NodeHierarchy::new()); + node_hierarchy.insert(node_id!("f"), NodeHierarchy::new()); + + let mut edges = IndexMap::::new(); + edges.insert(edge_id!("a__b"), [node_id!("a"), node_id!("b")]); + edges.insert(edge_id!("a__c"), [node_id!("a"), node_id!("c")]); + edges.insert(edge_id!("b__e"), [node_id!("b"), node_id!("e")]); + edges.insert(edge_id!("c__d"), [node_id!("c"), node_id!("d")]); + edges.insert(edge_id!("d__e"), [node_id!("d"), node_id!("e")]); + edges.insert(edge_id!("f__e"), [node_id!("f"), node_id!("e")]); + + let mut node_infos = IndexMap::::new(); + node_infos.insert(node_id!("a"), NodeInfo::new(String::from("a"))); + node_infos.insert(node_id!("b"), NodeInfo::new(String::from("b"))); + node_infos.insert(node_id!("c"), NodeInfo::new(String::from("c"))); + node_infos.insert(node_id!("d"), NodeInfo::new(String::from("d"))); + node_infos.insert(node_id!("e"), NodeInfo::new(String::from("e"))); + node_infos.insert(node_id!("f"), NodeInfo::new(String::from("f"))); + + InfoGraph::builder() + .with_direction(GraphDir::Vertical) + .with_hierarchy(node_hierarchy) + .with_node_infos(node_infos) + .with_edges(edges) + .build() + }; + + assert_eq!(info_graph_expected, info_graph); + Ok(()) +} + +#[test] +fn to_outcome_info_graph() -> Result<(), Box> { + let flow_spec_info = flow_spec_info()?; + + let info_graph = flow_spec_info.to_outcome_info_graph(); + + let info_graph_expected = { + let mut node_hierarchy = NodeHierarchy::new(); + node_hierarchy.insert(node_id!("a"), { + let mut node_hierarchy_a = NodeHierarchy::new(); + node_hierarchy_a.insert(node_id!("b"), NodeHierarchy::new()); + node_hierarchy_a + }); + node_hierarchy.insert(node_id!("c"), { + let mut node_hierarchy_c = NodeHierarchy::new(); + node_hierarchy_c.insert(node_id!("d"), NodeHierarchy::new()); + node_hierarchy_c + }); + node_hierarchy.insert(node_id!("e"), NodeHierarchy::new()); + node_hierarchy.insert(node_id!("f"), NodeHierarchy::new()); + + let mut edges = IndexMap::::new(); + edges.insert(edge_id!("a__c"), [node_id!("a"), node_id!("c")]); + edges.insert(edge_id!("b__e"), [node_id!("b"), node_id!("e")]); + edges.insert(edge_id!("d__e"), [node_id!("d"), node_id!("e")]); + edges.insert(edge_id!("f__e"), [node_id!("f"), node_id!("e")]); + + let mut node_infos = IndexMap::::new(); + node_infos.insert(node_id!("a"), NodeInfo::new(String::from("a"))); + node_infos.insert(node_id!("b"), NodeInfo::new(String::from("b"))); + node_infos.insert(node_id!("c"), NodeInfo::new(String::from("c"))); + node_infos.insert(node_id!("d"), NodeInfo::new(String::from("d"))); + node_infos.insert(node_id!("e"), NodeInfo::new(String::from("e"))); + node_infos.insert(node_id!("f"), NodeInfo::new(String::from("f"))); + + InfoGraph::builder() + .with_direction(GraphDir::Vertical) + .with_hierarchy(node_hierarchy) + .with_node_infos(node_infos) + .with_edges(edges) + .build() + }; + + assert_eq!(info_graph_expected, info_graph); + Ok(()) +} + #[test] fn clone() -> Result<(), Box> { let flow_spec_info = flow_spec_info()?; @@ -24,7 +125,7 @@ fn debug() -> Result<(), Box> { "FlowSpecInfo { \ flow_id: FlowId(\"flow_id\"), \ graph_info: GraphInfo { \ - graph: Dag { graph: Graph { Ty: \"Directed\", node_count: 6, edge_count: 9, edges: (0, 1), (0, 2), (1, 4), (2, 3), (3, 4), (5, 4), (1, 2), (5, 1), (0, 5), node weights: {0: ItemSpecInfo { item_id: ItemId(\"a\") }, 1: ItemSpecInfo { item_id: ItemId(\"b\") }, 2: ItemSpecInfo { item_id: ItemId(\"c\") }, 3: ItemSpecInfo { item_id: ItemId(\"d\") }, 4: ItemSpecInfo { item_id: ItemId(\"e\") }, 5: ItemSpecInfo { item_id: ItemId(\"f\") }}, edge weights: {0: Logic, 1: Logic, 2: Logic, 3: Logic, 4: Logic, 5: Logic, 6: Data, 7: Data, 8: Data} }, cycle_state: DfsSpace { dfs: Dfs { stack: [], discovered: FixedBitSet { data: [], length: 0 } } } } \ + graph: Dag { graph: Graph { Ty: \"Directed\", node_count: 6, edge_count: 9, edges: (0, 1), (0, 2), (1, 4), (2, 3), (3, 4), (5, 4), (1, 2), (5, 1), (0, 5), node weights: {0: ItemSpecInfo { item_id: ItemId(\"a\") }, 1: ItemSpecInfo { item_id: ItemId(\"b\") }, 2: ItemSpecInfo { item_id: ItemId(\"c\") }, 3: ItemSpecInfo { item_id: ItemId(\"d\") }, 4: ItemSpecInfo { item_id: ItemId(\"e\") }, 5: ItemSpecInfo { item_id: ItemId(\"f\") }}, edge weights: {0: Contains, 1: Logic, 2: Logic, 3: Contains, 4: Logic, 5: Logic, 6: Data, 7: Data, 8: Data} }, cycle_state: DfsSpace { dfs: Dfs { stack: [], discovered: FixedBitSet { data: [], length: 0 } } } } \ } \ }", format!("{flow_spec_info:?}") @@ -52,7 +153,7 @@ graph_info: edges: - - 0 - 1 - - Logic + - Contains - - 0 - 2 - Logic @@ -61,7 +162,7 @@ graph_info: - Logic - - 2 - 3 - - Logic + - Contains - - 3 - 4 - Logic @@ -103,10 +204,10 @@ graph_info: node_holes: [] edge_property: directed edges: - - [0, 1, Logic] + - [0, 1, Contains] - [0, 2, Logic] - [1, 4, Logic] - - [2, 3, Logic] + - [2, 3, Contains] - [3, 4, Logic] - [5, 4, Logic] - [1, 2, Data] @@ -127,11 +228,27 @@ fn flow_spec_info() -> Result> { } fn complex_graph() -> Result, WouldCycle> { + // Progress: + // + // ```text // a - b --------- e // \ / / // '-- c - d / // / // f --------' + // ``` + // + // Outcome: + // + // ```text + // .-a---. .-e-. + // |.-b-.| '---' + // |'---'| .-c---. + // '-----' |.-d-.| + // |'---'| + // .-f-. '-----' + // '---' + // ``` let mut item_graph_builder = ItemGraphBuilder::new(); let [fn_id_a, fn_id_b, fn_id_c, fn_id_d, fn_id_e, fn_id_f] = item_graph_builder.add_fns([ BlankItem::<()>::new(item_id!("a")).into(), @@ -141,14 +258,13 @@ fn complex_graph() -> Result, WouldCycle> { BlankItem::<()>::new(item_id!("e")).into(), BlankItem::<()>::new(item_id!("f")).into(), ]); - item_graph_builder.add_logic_edges([ - (fn_id_a, fn_id_b), - (fn_id_a, fn_id_c), - (fn_id_b, fn_id_e), - (fn_id_c, fn_id_d), - (fn_id_d, fn_id_e), - (fn_id_f, fn_id_e), - ])?; + item_graph_builder.add_contains_edge(fn_id_a, fn_id_b)?; + item_graph_builder.add_logic_edge(fn_id_a, fn_id_c)?; + item_graph_builder.add_logic_edge(fn_id_b, fn_id_e)?; + item_graph_builder.add_contains_edge(fn_id_c, fn_id_d)?; + item_graph_builder.add_logic_edge(fn_id_d, fn_id_e)?; + item_graph_builder.add_logic_edge(fn_id_f, fn_id_e)?; + let item_graph = item_graph_builder.build(); Ok(item_graph) } From 3cf4509f795f2e1a6c9104c2d2c56a28fd93b349 Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Fri, 15 Mar 2024 18:53:28 +1300 Subject: [PATCH 38/38] Address clippy lints. --- crate/params/src/any_spec_data_type.rs | 2 ++ crate/webi_components/src/home.rs | 1 + .../envman/src/items/peace_aws_s3_bucket/s3_bucket_state.rs | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/crate/params/src/any_spec_data_type.rs b/crate/params/src/any_spec_data_type.rs index cd2f08481..a3b8baa1f 100644 --- a/crate/params/src/any_spec_data_type.rs +++ b/crate/params/src/any_spec_data_type.rs @@ -1,3 +1,5 @@ +#![allow(clippy::multiple_bound_locations)] // https://github.com/marcianx/downcast-rs/issues/19 + use std::{any::Any, fmt}; use dyn_clone::DynClone; diff --git a/crate/webi_components/src/home.rs b/crate/webi_components/src/home.rs index 61cd5e089..df5e37682 100644 --- a/crate/webi_components/src/home.rs +++ b/crate/webi_components/src/home.rs @@ -4,6 +4,7 @@ use leptos_router::{Route, Router, Routes}; use crate::FlowGraph; +/// Top level component of the `WebiOutput`. #[component] pub fn Home() -> impl IntoView { // Provides context that manages stylesheets, titles, meta tags, etc. diff --git a/examples/envman/src/items/peace_aws_s3_bucket/s3_bucket_state.rs b/examples/envman/src/items/peace_aws_s3_bucket/s3_bucket_state.rs index 46976f843..d9a690472 100644 --- a/examples/envman/src/items/peace_aws_s3_bucket/s3_bucket_state.rs +++ b/examples/envman/src/items/peace_aws_s3_bucket/s3_bucket_state.rs @@ -17,7 +17,7 @@ pub enum S3BucketState { /// /// TODO: newtype + proc macro. name: String, - /// + /// Timestamp that the S3Bucket was created. creation_date: Timestamped>, }, }