diff --git a/.cargo/audit.toml b/.cargo/audit.toml new file mode 100644 index 000000000..8445275fd --- /dev/null +++ b/.cargo/audit.toml @@ -0,0 +1,8 @@ +[advisories] +ignore = [ + # `proc-macro-error` is Unmaintained. + # + # Transitive dependency of `syn_derive`. + # Pending https://github.com/Kyuuhachi/syn_derive/issues/4. + "RUSTSEC-2024-0370", +] diff --git a/.cargo/config.toml b/.cargo/config.toml index a5ee9520d..7ba167f0d 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,4 +1,17 @@ [alias] +clippy_cli = [ + "clippy", + "--workspace", + "--features", + "cli error_reporting output_progress item_interactions item_state_example", + "--fix", + "--exclude", + "peace_rt_model_web", + "--", + "-D", + "warnings", +] + # Nextest for different feature combinations test_0 = ["nextest", "run", "--workspace", "--no-default-features"] test_1 = ["nextest", "run", "--workspace", "--all-features"] @@ -10,5 +23,30 @@ coverage_1 = ["llvm-cov", "--no-report", "nextest", "--workspace", "--all-featur coverage_merge = 'llvm-cov report --lcov --output-path ./target/coverage/lcov.info' coverage_open = 'llvm-cov report --open --output-dir ./target/coverage' +# Build envman example +# cargo leptos build --project "envman" --features "item_interactions item_state_example" --bin-features "cli" --release +envman_build_debug = [ + "leptos", + "build", + "--project", + "envman", + "--features", + "item_interactions item_state_example", + "--bin-features", + "cli", +] + +envman_build_release = [ + "leptos", + "build", + "--project", + "envman", + "--features", + "item_interactions item_state_example", + "--bin-features", + "cli", + "--release", +] + [env] CLICOLOR_FORCE = "1" diff --git a/.github/workflows/book.yml b/.github/workflows/book.yml index cdfa9a4ef..529570783 100644 --- a/.github/workflows/book.yml +++ b/.github/workflows/book.yml @@ -20,24 +20,24 @@ jobs: - name: 'Install `wasm-pack`' uses: jetli/wasm-pack-action@v0.4.0 with: - version: 'v0.11.1' + version: 'v0.13.0' - name: mdbook Cache id: mdbook_cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cargo/bin/mdbook key: ${{ runner.os }}-mdbook - name: mdbook-graphviz Cache id: mdbook_graphviz_cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cargo/bin/mdbook-graphviz key: ${{ runner.os }}-mdbook-graphviz - name: Setup Graphviz - uses: ts-graphviz/setup-graphviz@v1 + uses: ts-graphviz/setup-graphviz@v2 - run: cargo install mdbook-graphviz if: steps.mdbook_graphviz_cache.outputs.cache-hit != 'true' @@ -56,12 +56,6 @@ jobs: --features 'error_reporting' done - # Build and publish book - # - name: Install `mdbook` - # uses: peaceiris/actions-mdbook@v1 - # with: - # mdbook-version: latest - # use custom version of mdbook for now - name: Install `mdbook` run: cargo install mdbook --git https://github.com/azriel91/mdBook.git --branch improvement/code-blocks @@ -77,7 +71,7 @@ jobs: - name: Publish to `gh-pages` if: ${{ github.ref == 'refs/heads/main' }} - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./doc/book diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0a7c9c49..45592eb3d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: timeout-minutes: 10 steps: - uses: actions/checkout@v4 - - uses: bp3d-actions/audit-check@9c23bd47e5e7b15b824739e0862cb878a52cc211 + - uses: actions-rust-lang/audit@v1 with: token: ${{ secrets.GITHUB_TOKEN }} @@ -28,7 +28,7 @@ jobs: - name: cargo-about cache id: cargo-about-cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cargo/bin/cargo-about key: cargo-about-${{ runner.os }} @@ -83,12 +83,7 @@ jobs: - name: 'Run clippy' # we cannot use `--all-features` because `envman` has features that are mutually exclusive. run: | - cargo clippy \ - --workspace \ - --features "cli error_reporting output_progress" \ - --fix \ - --exclude peace_rt_model_web \ - -- -D warnings + cargo clippy_cli # Ideally we'd also run it for WASM, but: # @@ -139,9 +134,10 @@ jobs: run: du -sh target/coverage target/llvm-cov-target - name: Upload to codecov.io - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: files: ./target/coverage/lcov.info + token: ${{ secrets.CODECOV_TOKEN }} build_and_test_linux: name: Build and Test (Linux) @@ -203,7 +199,7 @@ jobs: - name: cargo-leptos cache id: cargo-leptos-cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cargo/bin/cargo-leptos key: cargo-leptos-${{ runner.os }} @@ -213,7 +209,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" --bin-features "cli" -v + run: cargo envman_build_debug # When updating this, also update book.yml - name: 'Example: download (WASM)' diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9b817ebea..3e2d74d4b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -12,7 +12,7 @@ jobs: timeout-minutes: 10 steps: - uses: actions/checkout@v4 - - uses: bp3d-actions/audit-check@9c23bd47e5e7b15b824739e0862cb878a52cc211 + - uses: actions-rust-lang/audit@v1 with: token: ${{ secrets.GITHUB_TOKEN }} @@ -58,7 +58,7 @@ jobs: - name: cargo-release Cache id: cargo_release_cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cargo/bin/cargo-release key: ${{ runner.os }}-cargo-release diff --git a/Cargo.toml b/Cargo.toml index 7082a9a45..45084e08f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ peace_diff = { workspace = true } peace_flow_model = { workspace = true } peace_fmt = { workspace = true } peace_params = { workspace = true } +peace_item_model = { workspace = true, optional = true } peace_resource_rt = { workspace = true } peace_rt = { workspace = true } peace_rt_model = { workspace = true } @@ -71,9 +72,26 @@ output_progress = [ "peace_cli?/output_progress", "peace_cmd_rt/output_progress", "peace_cfg/output_progress", + "peace_item_model/output_progress", "peace_rt/output_progress", "peace_rt_model/output_progress", "peace_webi?/output_progress", + "peace_cmd_model/output_progress", +] +item_interactions = [ + "dep:peace_item_model", + "peace_cfg/item_interactions", + "peace_webi?/item_interactions", + "peace_webi_components?/item_interactions", +] +item_state_example = [ + "peace_cfg/item_state_example", + "peace_cmd/item_state_example", + "peace_data/item_state_example", + "peace_params/item_state_example", + "peace_rt_model/item_state_example", + "peace_webi?/item_state_example", + "peace_webi_components?/item_state_example", ] ssr = [ "peace_webi?/ssr", @@ -118,6 +136,7 @@ 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" } +peace_item_model = { path = "crate/item_model", version = "0.0.13" } peace_resource_rt = { path = "crate/resource_rt", version = "0.0.13" } peace_rt = { path = "crate/rt", version = "0.0.13" } peace_rt_model = { path = "crate/rt_model", version = "0.0.13" } @@ -144,55 +163,58 @@ 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.22.0" -bytes = "1.5.0" +async-trait = "0.1.81" +axum = "0.7.5" +base64 = "0.22.1" +bytes = "1.7.1" cfg-if = "1.0.0" -chrono = { version = "0.4.35", default-features = false, features = ["clock", "serde"] } +chrono = { version = "0.4.38", 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 = { version = "0.5.0", default-features = false } +dot_ix = { version = "0.8.1", default-features = false } +dot_ix_model = "0.8.1" +downcast-rs = "1.2.1" dyn-clone = "1.0.17" enser = "0.1.4" -erased-serde = "0.4.3" -fn_graph = { version = "0.13.2", features = ["async", "graph_info", "interruptible", "resman"] } +erased-serde = "0.4.5" +fn_graph = { version = "0.13.3", features = ["async", "graph_info", "interruptible", "resman"] } futures = "0.3.30" -heck = "0.4.1" -indexmap = "2.2.5" +gloo-timers = "0.3.0" +heck = "0.5.0" +indexmap = "2.5.0" indicatif = "0.17.8" -interruptible = "0.2.2" +interruptible = "0.2.4" leptos = { version = "0.6" } leptos_axum = "0.6" leptos_meta = { version = "0.6" } leptos_router = { version = "0.6" } -libc = "0.2.153" +libc = "0.2.158" miette = "7.2.0" -own = "0.1.0" +own = "0.1.3" pretty_assertions = "1.4.0" -proc-macro2 = "1.0.78" -quote = "1.0.35" +proc-macro2 = "1.0.86" +quote = "1.0.37" raw_tty = "0.1.0" -reqwest = "0.11.25" -resman = "0.17.0" -serde = "1.0.197" +reqwest = "0.12.7" +resman = "0.17.2" +serde = "1.0.209" serde-wasm-bindgen = "0.6.5" -serde_json = "1.0.114" -serde_yaml = "0.9.32" -syn = "2.0.52" -tar = "0.4.40" -tempfile = "3.10.1" -thiserror = "1.0.57" -tokio = "1.36" -tokio-util = "0.7.10" +serde_json = "1.0.127" +serde_yaml = "0.9.34" +smallvec = "1.13.2" +syn = "2.0.77" +tar = "0.4.41" +tempfile = "3.12.0" +thiserror = "1.0.63" +tokio = "1.40" +tokio-util = "0.7.11" 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.92" -web-sys = "0.3.69" +url = "2.5.2" +wasm-bindgen = "0.2.95" +web-sys = "0.3.70" [workspace.lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } diff --git a/crate/cfg/Cargo.toml b/crate/cfg/Cargo.toml index f59753caa..d13b2031f 100644 --- a/crate/cfg/Cargo.toml +++ b/crate/cfg/Cargo.toml @@ -26,6 +26,7 @@ enser = { workspace = true } peace_core = { workspace = true } peace_data = { workspace = true } peace_params = { workspace = true } +peace_item_model = { workspace = true, optional = true } peace_resource_rt = { workspace = true } serde = { workspace = true, features = ["derive"] } tynm = { workspace = true } @@ -33,4 +34,10 @@ tynm = { workspace = true } [features] default = [] error_reporting = ["peace_params/error_reporting"] -output_progress = ["peace_core/output_progress"] +output_progress = [ + "dep:peace_item_model", + "peace_core/output_progress", + "peace_item_model/output_progress", +] +item_interactions = ["dep:peace_item_model"] +item_state_example = [] diff --git a/crate/cfg/src/accessors/stored.rs b/crate/cfg/src/accessors/stored.rs index c4c821429..cb5ea6677 100644 --- a/crate/cfg/src/accessors/stored.rs +++ b/crate/cfg/src/accessors/stored.rs @@ -60,7 +60,7 @@ where } } -impl<'borrow, T> DataAccess for Stored<'borrow, T> { +impl DataAccess for Stored<'_, T> { fn borrows() -> TypeIds where Self: Sized, @@ -78,7 +78,7 @@ impl<'borrow, T> DataAccess for Stored<'borrow, T> { } } -impl<'borrow, T> DataAccessDyn for Stored<'borrow, T> { +impl DataAccessDyn for Stored<'_, T> { fn borrows(&self) -> TypeIds where Self: Sized, diff --git a/crate/cfg/src/item.rs b/crate/cfg/src/item.rs index d5a74e184..7fb1c06ca 100644 --- a/crate/cfg/src/item.rs +++ b/crate/cfg/src/item.rs @@ -77,6 +77,7 @@ pub trait Item: DynClone { /// /// [state concept]: https://peace.mk/book/technical_concepts/state.html /// [`State`]: crate::state::State + #[cfg(not(feature = "output_progress"))] type State: Clone + Debug + Display @@ -87,6 +88,36 @@ pub trait Item: DynClone { + Sync + 'static; + /// Summary of the managed item's state. + /// + /// **For an extensive explanation of state, and how to define it, please + /// see the [state concept] as well as the [`State`] type.** + /// + /// This type is used to represent the current state of the item (if it + /// exists), the goal state of the item (what is intended to exist), and + /// is used in the *diff* calculation -- what is the difference between the + /// current and goal states. + /// + /// # Examples + /// + /// * A file's state may be its path, and a hash of its contents. + /// * A server's state may be its operating system, CPU and memory capacity, + /// IP address, and ID. + /// + /// [state concept]: https://peace.mk/book/technical_concepts/state.html + /// [`State`]: crate::state::State + #[cfg(feature = "output_progress")] + type State: Clone + + Debug + + Display + + PartialEq + + Serialize + + DeserializeOwned + + Send + + Sync + + 'static + + crate::RefInto; + /// Diff between the current and target [`State`]s. /// /// # Design Note @@ -174,10 +205,60 @@ pub trait Item: DynClone { /// must be inserted into the map so that item functions can borrow the /// instance of that type. /// + /// ## External Parameters + /// + /// If the item works with an external source for parameters, such as: + /// + /// * a version controlled package file that specifies dependency versions + /// * (discouraged) a web service with project configuration + /// + /// then this is the function to include the logic to read those files. + /// + /// ## Fallibility + /// + /// The function signature allows for fallibility, to allow issues to be + /// reported early, such as: + /// + /// * Credentials to SDK clients not present on the user's system. + /// * Incompatible / invalid values specified in project configuration + /// files, or expected project configuration files don't exist. + /// /// [`check`]: crate::ApplyFns::check /// [`apply`]: crate::ApplyFns::apply async fn setup(&self, resources: &mut Resources) -> Result<(), Self::Error>; + /// Returns an example fully deployed state of the managed item. + /// + /// # Implementors + /// + /// This is *expected* to always return a value, as it is used to: + /// + /// * Display a diagram that shows the user what the item looks like when it + /// is fully deployed, without actually interacting with any external + /// state. + /// + /// As much as possible, use the values in the provided params and data. + /// + /// This function should **NOT** interact with any external services, or + /// read from files that are part of the automation process, e.g. + /// querying data from a web endpoint, or reading files that may be + /// downloaded by a predecessor. + /// + /// ## Infallibility + /// + /// The signature is deliberately infallible to signal to implementors that + /// calling an external service / read from a file is incorrect + /// implementation for this method -- values in params / data may be example + /// values from other items that may not resolve. + /// + /// ## Non-async + /// + /// Similar to infallibility, this signals to implementors that this + /// function should be a cheap example state computation that is relatively + /// realistic rather than determining an accurate value. + #[cfg(feature = "item_state_example")] + fn state_example(params: &Self::Params<'_>, data: Self::Data<'_>) -> Self::State; + /// Returns the current state of the managed item, if possible. /// /// This should return `Ok(None)` if the state is not able to be queried, @@ -315,16 +396,19 @@ pub trait Item: DynClone { /// This should mirror the logic in [`apply`], with the following /// differences: /// - /// * When state will actually be altered, this would skip the logic. + /// 1. When state will actually be altered, this would skip the logic. /// - /// * Where there would be IDs received from an external system, a + /// 2. Where there would be IDs received from an external system, a /// placeholder ID should still be inserted into the runtime data. This /// should allow subsequent `Item`s that rely on this one to use those /// placeholders in their logic. /// /// # Implementors /// - /// This function call is intended to be read-only and cheap. + /// This function call is intended to be read-only and relatively cheap. + /// Values in `params` and `data` cannot be guaranteed to truly exist. + /// [#196] tracks the work to resolve what this function's contract should + /// be. /// /// # Parameters /// @@ -345,6 +429,7 @@ pub trait Item: DynClone { /// [`state_goal`]: crate::Item::state_goal /// [`State`]: Self::State /// [`state_diff`]: crate::Item::state_diff + /// [#196]: https://github.com/azriel91/peace/issues/196 async fn apply_dry( fn_ctx: FnCtx<'_>, params: &Self::Params<'_>, @@ -385,4 +470,45 @@ pub trait Item: DynClone { state_target: &Self::State, diff: &Self::StateDiff, ) -> Result; + + /// Returns the physical resources that this item interacts with. + /// + /// # Examples + /// + /// ## File Download Item + /// + /// This may be from: + /// + /// * host server + /// * URL + /// + /// to: + /// + /// * localhost + /// * file system path + /// + /// + /// ### Server Launch Item + /// + /// This may be from: + /// + /// * localhost + /// + /// to: + /// + /// * cloud provider + /// * region + /// * subnet + /// * host + /// + /// + /// # Implementors + /// + /// The returned list should be in order of least specific to most specific + /// location. + #[cfg(feature = "item_interactions")] + fn interactions( + params: &Self::Params<'_>, + data: Self::Data<'_>, + ) -> Vec; } diff --git a/crate/cfg/src/lib.rs b/crate/cfg/src/lib.rs index 2a0e79555..3626665a5 100644 --- a/crate/cfg/src/lib.rs +++ b/crate/cfg/src/lib.rs @@ -12,8 +12,14 @@ pub use peace_core::*; pub use crate::{fn_ctx::FnCtx, item::Item, state::State}; +#[cfg(feature = "output_progress")] +pub use crate::ref_into::RefInto; + pub mod accessors; pub mod state; mod fn_ctx; mod item; + +#[cfg(feature = "output_progress")] +mod ref_into; diff --git a/crate/cfg/src/ref_into.rs b/crate/cfg/src/ref_into.rs new file mode 100644 index 000000000..412294cf8 --- /dev/null +++ b/crate/cfg/src/ref_into.rs @@ -0,0 +1,26 @@ +use peace_item_model::ItemLocationState; + +/// Returns `T` from a reference to `self`. +/// +/// Allows setting a constraint on `Item::State`, such that `&State` can be +/// turned into an `peace_item_model::ItemLocationState`. +/// +/// # Implementors +/// +/// You should `impl<'state> From<&'state YourItemState> for ItemLocationState +/// {}`. There is a blanket implementation that implements +/// `RefInto for S where ItemLocationState: From<&'state S>` +pub trait RefInto { + /// Returns `T` from a reference to `self`. + fn into(&self) -> T; +} + +impl RefInto for S +where + for<'state> ItemLocationState: From<&'state S>, + S: 'static, +{ + fn into(&self) -> ItemLocationState { + ItemLocationState::from(self) + } +} diff --git a/crate/cli/Cargo.toml b/crate/cli/Cargo.toml index a51b8ee74..035ecc567 100644 --- a/crate/cli/Cargo.toml +++ b/crate/cli/Cargo.toml @@ -24,7 +24,9 @@ cfg-if = { workspace = true } console = { workspace = true } futures = { workspace = true } peace_cli_model = { workspace = true } +peace_cmd_model = { workspace = true, optional = true } peace_core = { workspace = true } +peace_item_model = { workspace = true, optional = true } peace_fmt = { workspace = true } peace_rt_model_core = { workspace = true } serde = { workspace = true } @@ -40,6 +42,10 @@ raw_tty = { workspace = true } default = [] output_in_memory = ["peace_rt_model_core/output_in_memory"] output_progress = [ + "dep:peace_cmd_model", + "dep:peace_item_model", + "peace_cmd_model/output_progress", "peace_core/output_progress", + "peace_item_model/output_progress", "peace_rt_model_core/output_progress", ] diff --git a/crate/cli/src/output/cli_output.rs b/crate/cli/src/output/cli_output.rs index c56d7f1ed..c0c98970a 100644 --- a/crate/cli/src/output/cli_output.rs +++ b/crate/cli/src/output/cli_output.rs @@ -12,14 +12,19 @@ use crate::output::{CliColorize, CliMdPresenter, CliOutputBuilder}; cfg_if::cfg_if! { if #[cfg(feature = "output_progress")] { - use peace_core::progress::{ - ProgressComplete, - ProgressLimit, - ProgressStatus, - ProgressTracker, - ProgressUpdate, - ProgressUpdateAndId, + use peace_core::{ + progress::{ + CmdBlockItemInteractionType, + ProgressComplete, + ProgressLimit, + ProgressStatus, + ProgressTracker, + ProgressUpdate, + ProgressUpdateAndId, + }, + ItemId, }; + use peace_item_model::ItemLocationState; use peace_rt_model_core::{ indicatif::{ProgressDrawTarget, ProgressStyle}, CmdProgressTracker, @@ -607,6 +612,21 @@ where } } + #[cfg(feature = "output_progress")] + async fn cmd_block_start( + &mut self, + _cmd_block_item_interaction_type: CmdBlockItemInteractionType, + ) { + } + + #[cfg(feature = "output_progress")] + async fn item_location_state( + &mut self, + _item_id: ItemId, + _item_location_state: ItemLocationState, + ) { + } + #[cfg(feature = "output_progress")] async fn progress_update( &mut self, @@ -688,6 +708,7 @@ where progress_bar.println(t_serialized); }); } + OutputFormat::None => {} } } CliProgressFormat::None => {} @@ -736,6 +757,7 @@ where self.output_json(&presentable, Error::StatesSerializeJson) .await } + OutputFormat::None => Ok(()), } } @@ -768,6 +790,7 @@ where .map_err(NativeError::StdoutWrite) .map_err(Error::Native)?; } + OutputFormat::None => {} } Ok(()) diff --git a/crate/cli/src/output/cli_output_builder.rs b/crate/cli/src/output/cli_output_builder.rs index e753817fc..2387abf0d 100644 --- a/crate/cli/src/output/cli_output_builder.rs +++ b/crate/cli/src/output/cli_output_builder.rs @@ -210,32 +210,39 @@ where }; #[cfg(feature = "output_progress")] - let progress_format = match progress_format { - CliProgressFormatOpt::Auto => { - // Even though we're using `tokio::io::stdout` / `stderr`, `IsTerminal` is only - // implemented on `std::io::stdout` / `stderr`. - match &progress_target { - CliOutputTarget::Stdout => { - if std::io::stdout().is_terminal() { - CliProgressFormat::ProgressBar - } else { - CliProgressFormat::Outcome + let progress_format = { + let progress_format = match progress_format { + CliProgressFormatOpt::Auto => { + // Even though we're using `tokio::io::stdout` / `stderr`, `IsTerminal` is only + // implemented on `std::io::stdout` / `stderr`. + match &progress_target { + CliOutputTarget::Stdout => { + if std::io::stdout().is_terminal() { + CliProgressFormat::ProgressBar + } else { + CliProgressFormat::Outcome + } } - } - CliOutputTarget::Stderr => { - if std::io::stderr().is_terminal() { - CliProgressFormat::ProgressBar - } else { - CliProgressFormat::Outcome + CliOutputTarget::Stderr => { + if std::io::stderr().is_terminal() { + CliProgressFormat::ProgressBar + } else { + CliProgressFormat::Outcome + } } + #[cfg(feature = "output_in_memory")] + CliOutputTarget::InMemory(_) => CliProgressFormat::ProgressBar, } - #[cfg(feature = "output_in_memory")] - CliOutputTarget::InMemory(_) => CliProgressFormat::ProgressBar, } + CliProgressFormatOpt::Outcome => CliProgressFormat::Outcome, + CliProgressFormatOpt::ProgressBar => CliProgressFormat::ProgressBar, + CliProgressFormatOpt::None => CliProgressFormat::None, + }; + + match (progress_format, outcome_format) { + (CliProgressFormat::Outcome, OutputFormat::None) => CliProgressFormat::None, + _ => progress_format, } - CliProgressFormatOpt::Outcome => CliProgressFormat::Outcome, - CliProgressFormatOpt::ProgressBar => CliProgressFormat::ProgressBar, - CliProgressFormatOpt::None => CliProgressFormat::None, }; // We need to suppress the `^C\n` characters that come through the terminal. diff --git a/crate/cli_model/src/output_format.rs b/crate/cli_model/src/output_format.rs index 47870d9fb..f0bc8cb07 100644 --- a/crate/cli_model/src/output_format.rs +++ b/crate/cli_model/src/output_format.rs @@ -15,6 +15,8 @@ pub enum OutputFormat { /// /// [JSON]: https://www.json.org/ Json, + /// Don't output anything. + None, } impl FromStr for OutputFormat { @@ -25,6 +27,7 @@ impl FromStr for OutputFormat { "text" => Ok(Self::Text), "yaml" => Ok(Self::Yaml), "json" => Ok(Self::Json), + "none" => Ok(Self::None), _ => Err(OutputFormatParseError(s.to_string())), } } diff --git a/crate/cmd/Cargo.toml b/crate/cmd/Cargo.toml index 163ccb77c..7650f607d 100644 --- a/crate/cmd/Cargo.toml +++ b/crate/cmd/Cargo.toml @@ -39,6 +39,11 @@ tokio = { workspace = true, features = ["fs"] } [features] default = [] +item_state_example = [ + "peace_cfg/item_state_example", + "peace_rt_model/item_state_example", + "peace_params/item_state_example", +] output_progress = [ "dep:indicatif", "peace_core/output_progress", diff --git a/crate/cmd/src/ctx/cmd_ctx_builder.rs b/crate/cmd/src/ctx/cmd_ctx_builder.rs index 0018ba1e5..64d7b9f03 100644 --- a/crate/cmd/src/ctx/cmd_ctx_builder.rs +++ b/crate/cmd/src/ctx/cmd_ctx_builder.rs @@ -54,6 +54,8 @@ where interruptibility: Interruptibility<'static>, /// Workspace that the `peace` tool runs in. workspace: OwnedOrRef<'ctx, Workspace>, + /// Runtime borrow-checked typemap of data available to the command context. + resources: Resources, /// Data held while building `CmdCtx`. scope_builder: ScopeBuilder, } @@ -371,6 +373,7 @@ where if params_no_issues { Ok(params_specs) } else { + let params_specs_stored_mismatches = Box::new(params_specs_stored_mismatches); Err(peace_rt_model::Error::ParamsSpecsMismatch { item_ids_with_no_params_specs, params_specs_provided_mismatches, diff --git a/crate/cmd/src/ctx/cmd_ctx_builder_types.rs b/crate/cmd/src/ctx/cmd_ctx_builder_types.rs index 609e79385..3b1462266 100644 --- a/crate/cmd/src/ctx/cmd_ctx_builder_types.rs +++ b/crate/cmd/src/ctx/cmd_ctx_builder_types.rs @@ -107,15 +107,15 @@ pub type CmdCtxTypesCollectorEmpty = CmdCtxBuilderTypesCollect >; impl< - AppError, - Output, - ParamsKeysT, - WorkspaceParamsSelection, - ProfileParamsSelection, - FlowParamsSelection, - ProfileSelection, - FlowSelection, -> CmdCtxBuilderTypes + AppError, + Output, + ParamsKeysT, + WorkspaceParamsSelection, + ProfileParamsSelection, + FlowParamsSelection, + ProfileSelection, + FlowSelection, + > CmdCtxBuilderTypes for CmdCtxBuilderTypesCollector< AppError, Output, diff --git a/crate/cmd/src/scopes/multi_profile_no_flow.rs b/crate/cmd/src/scopes/multi_profile_no_flow.rs index b5ac5dd35..fe8397e8b 100644 --- a/crate/cmd/src/scopes/multi_profile_no_flow.rs +++ b/crate/cmd/src/scopes/multi_profile_no_flow.rs @@ -305,8 +305,8 @@ where } } -impl<'ctx, CmdCtxTypesT, WorkspaceParamsK, ProfileParamsKMaybe, FlowParamsKMaybe> - MultiProfileNoFlow<'ctx, CmdCtxTypesT> +impl + MultiProfileNoFlow<'_, CmdCtxTypesT> where CmdCtxTypesT: CmdCtxTypes< ParamsKeys = ParamsKeysImpl< @@ -326,8 +326,8 @@ where } } -impl<'ctx, CmdCtxTypesT, WorkspaceParamsKMaybe, ProfileParamsK, FlowParamsKMaybe> - MultiProfileNoFlow<'ctx, CmdCtxTypesT> +impl + MultiProfileNoFlow<'_, CmdCtxTypesT> where CmdCtxTypesT: CmdCtxTypes< ParamsKeys = ParamsKeysImpl< diff --git a/crate/cmd/src/scopes/multi_profile_single_flow.rs b/crate/cmd/src/scopes/multi_profile_single_flow.rs index 4b4e63c9c..745aa553f 100644 --- a/crate/cmd/src/scopes/multi_profile_single_flow.rs +++ b/crate/cmd/src/scopes/multi_profile_single_flow.rs @@ -98,7 +98,7 @@ where /// Directories of each profile's execution history. profile_history_dirs: BTreeMap, /// The chosen process flow. - flow: &'ctx Flow, + flow: OwnedOrRef<'ctx, Flow>, /// Flow directory that stores params and states. flow_dirs: BTreeMap, /// Type registries for [`WorkspaceParams`], [`ProfileParams`], and @@ -236,7 +236,7 @@ where profiles: Vec, profile_dirs: BTreeMap, profile_history_dirs: BTreeMap, - flow: &'ctx Flow, + flow: OwnedOrRef<'ctx, Flow>, flow_dirs: BTreeMap, params_type_regs: ParamsTypeRegs, workspace_params: WorkspaceParams< @@ -283,7 +283,7 @@ where } } -impl<'ctx, CmdCtxTypesT> MultiProfileSingleFlow<'ctx, CmdCtxTypesT> +impl MultiProfileSingleFlow<'_, CmdCtxTypesT> where CmdCtxTypesT: CmdCtxTypes, { @@ -391,7 +391,7 @@ where /// Returns the flow. pub fn flow(&self) -> &Flow { - self.flow + &self.flow } /// Returns the flow directories keyed by each profile. @@ -456,8 +456,8 @@ where } } -impl<'ctx, CmdCtxTypesT, WorkspaceParamsK, ProfileParamsKMaybe, FlowParamsKMaybe> - MultiProfileSingleFlow<'ctx, CmdCtxTypesT> +impl + MultiProfileSingleFlow<'_, CmdCtxTypesT> where CmdCtxTypesT: CmdCtxTypes< ParamsKeys = ParamsKeysImpl< @@ -477,8 +477,8 @@ where } } -impl<'ctx, CmdCtxTypesT, WorkspaceParamsKMaybe, ProfileParamsK, FlowParamsKMaybe> - MultiProfileSingleFlow<'ctx, CmdCtxTypesT> +impl + MultiProfileSingleFlow<'_, CmdCtxTypesT> where CmdCtxTypesT: CmdCtxTypes< ParamsKeys = ParamsKeysImpl< @@ -498,8 +498,8 @@ where } } -impl<'ctx, CmdCtxTypesT, WorkspaceParamsKMaybe, ProfileParamsKMaybe, FlowParamsK> - MultiProfileSingleFlow<'ctx, CmdCtxTypesT> +impl + MultiProfileSingleFlow<'_, CmdCtxTypesT> where CmdCtxTypesT: CmdCtxTypes< ParamsKeys = ParamsKeysImpl< diff --git a/crate/cmd/src/scopes/no_profile_no_flow.rs b/crate/cmd/src/scopes/no_profile_no_flow.rs index 567e77915..58432815b 100644 --- a/crate/cmd/src/scopes/no_profile_no_flow.rs +++ b/crate/cmd/src/scopes/no_profile_no_flow.rs @@ -132,8 +132,8 @@ where } } -impl<'ctx, CmdCtxTypesT, WorkspaceParamsK, ProfileParamsKMaybe, FlowParamsKMaybe> - NoProfileNoFlow<'ctx, CmdCtxTypesT> +impl + NoProfileNoFlow<'_, CmdCtxTypesT> where CmdCtxTypesT: CmdCtxTypes< ParamsKeys = ParamsKeysImpl< diff --git a/crate/cmd/src/scopes/single_profile_no_flow.rs b/crate/cmd/src/scopes/single_profile_no_flow.rs index 9a62293cd..fb27d8112 100644 --- a/crate/cmd/src/scopes/single_profile_no_flow.rs +++ b/crate/cmd/src/scopes/single_profile_no_flow.rs @@ -279,8 +279,8 @@ where } } -impl<'ctx, CmdCtxTypesT, WorkspaceParamsK, ProfileParamsKMaybe, FlowParamsKMaybe> - SingleProfileNoFlow<'ctx, CmdCtxTypesT> +impl + SingleProfileNoFlow<'_, CmdCtxTypesT> where CmdCtxTypesT: CmdCtxTypes< ParamsKeys = ParamsKeysImpl< @@ -300,8 +300,8 @@ where } } -impl<'ctx, CmdCtxTypesT, WorkspaceParamsKMaybe, ProfileParamsK, FlowParamsKMaybe> - SingleProfileNoFlow<'ctx, CmdCtxTypesT> +impl + SingleProfileNoFlow<'_, CmdCtxTypesT> where CmdCtxTypesT: CmdCtxTypes< ParamsKeys = ParamsKeysImpl< diff --git a/crate/cmd/src/scopes/single_profile_single_flow.rs b/crate/cmd/src/scopes/single_profile_single_flow.rs index fdbab3e75..32d3ca642 100644 --- a/crate/cmd/src/scopes/single_profile_single_flow.rs +++ b/crate/cmd/src/scopes/single_profile_single_flow.rs @@ -80,7 +80,7 @@ where /// Directory to store profile execution history. profile_history_dir: ProfileHistoryDir, /// The chosen process flow. - flow: &'ctx Flow, + flow: OwnedOrRef<'ctx, Flow>, /// Flow directory that stores params and states. flow_dir: FlowDir, /// Type registries for [`WorkspaceParams`], [`ProfileParams`], and @@ -255,7 +255,7 @@ where profile: Profile, profile_dir: ProfileDir, profile_history_dir: ProfileHistoryDir, - flow: &'ctx Flow, + flow: OwnedOrRef<'ctx, Flow>, flow_dir: FlowDir, params_type_regs: ParamsTypeRegs, workspace_params: WorkspaceParams< @@ -295,7 +295,7 @@ where } } -impl<'ctx, CmdCtxTypesT> SingleProfileSingleFlow<'ctx, CmdCtxTypesT> +impl SingleProfileSingleFlow<'_, CmdCtxTypesT> where CmdCtxTypesT: CmdCtxTypes, { @@ -461,7 +461,7 @@ where /// Returns a reference to the flow. pub fn flow(&self) -> &Flow { - self.flow + &self.flow } /// Returns a reference to the flow directory. @@ -517,8 +517,8 @@ where } } -impl<'ctx, CmdCtxTypesT, WorkspaceParamsK, ProfileParamsKMaybe, FlowParamsKMaybe> - SingleProfileSingleFlow<'ctx, CmdCtxTypesT> +impl + SingleProfileSingleFlow<'_, CmdCtxTypesT> where CmdCtxTypesT: CmdCtxTypes< ParamsKeys = ParamsKeysImpl< @@ -538,8 +538,8 @@ where } } -impl<'ctx, CmdCtxTypesT, WorkspaceParamsKMaybe, ProfileParamsK, FlowParamsKMaybe> - SingleProfileSingleFlow<'ctx, CmdCtxTypesT> +impl + SingleProfileSingleFlow<'_, CmdCtxTypesT> where CmdCtxTypesT: CmdCtxTypes< ParamsKeys = ParamsKeysImpl< @@ -559,8 +559,8 @@ where } } -impl<'ctx, CmdCtxTypesT, WorkspaceParamsKMaybe, ProfileParamsKMaybe, FlowParamsK> - SingleProfileSingleFlow<'ctx, CmdCtxTypesT> +impl + SingleProfileSingleFlow<'_, CmdCtxTypesT> where CmdCtxTypesT: CmdCtxTypes< ParamsKeys = ParamsKeysImpl< diff --git a/crate/cmd/src/scopes/type_params/flow_selection.rs b/crate/cmd/src/scopes/type_params/flow_selection.rs index 6278f725b..43b0cc98a 100644 --- a/crate/cmd/src/scopes/type_params/flow_selection.rs +++ b/crate/cmd/src/scopes/type_params/flow_selection.rs @@ -1,3 +1,4 @@ +use own::OwnedOrRef; use peace_rt_model::Flow; /// A `Flow` is not yet selected. @@ -6,4 +7,4 @@ pub struct FlowNotSelected; /// A `Flow` is selected. #[derive(Debug)] -pub struct FlowSelected<'ctx, E>(pub(crate) &'ctx Flow); +pub struct FlowSelected<'ctx, E>(pub(crate) OwnedOrRef<'ctx, Flow>); diff --git a/crate/cmd/src/scopes/type_params/profile_selection.rs b/crate/cmd/src/scopes/type_params/profile_selection.rs index 5870178f0..8247db524 100644 --- a/crate/cmd/src/scopes/type_params/profile_selection.rs +++ b/crate/cmd/src/scopes/type_params/profile_selection.rs @@ -1,5 +1,6 @@ use std::fmt; +use own::OwnedOrRef; use peace_core::Profile; /// A `Profile` is not yet selected. @@ -13,12 +14,14 @@ pub struct ProfileSelected(pub(crate) Profile); /// The `Profile` will be read from workspace params using the provided key /// during command context build. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct ProfileFromWorkspaceParam<'key, WorkspaceParamsK>(pub(crate) &'key WorkspaceParamsK); +pub struct ProfileFromWorkspaceParam<'key, WorkspaceParamsK>( + pub(crate) OwnedOrRef<'key, WorkspaceParamsK>, +); /// Filter function for `MultiProfile` scopes. pub struct ProfileFilterFn<'f>(pub(crate) Box bool + 'f>); -impl<'f> fmt::Debug for ProfileFilterFn<'f> { +impl fmt::Debug for ProfileFilterFn<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("ProfileFilterFn") .field(&"Box bool") diff --git a/crate/cmd_model/Cargo.toml b/crate/cmd_model/Cargo.toml index 654d285f8..8e5fdb993 100644 --- a/crate/cmd_model/Cargo.toml +++ b/crate/cmd_model/Cargo.toml @@ -24,10 +24,12 @@ fn_graph = { workspace = true } futures = { workspace = true } miette = { workspace = true, optional = true } indexmap = { workspace = true } -peace_cfg = { workspace = true } +peace_core = { workspace = true } +serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } tynm = { workspace = true } [features] default = [] error_reporting = ["dep:miette"] +output_progress = [] diff --git a/crate/cmd_model/src/cmd_block_outcome.rs b/crate/cmd_model/src/cmd_block_outcome.rs index 5092d7095..76dd6c541 100644 --- a/crate/cmd_model/src/cmd_block_outcome.rs +++ b/crate/cmd_model/src/cmd_block_outcome.rs @@ -1,6 +1,6 @@ use fn_graph::StreamOutcome; use indexmap::IndexMap; -use peace_cfg::ItemId; +use peace_core::ItemId; use crate::{StreamOutcomeAndErrors, ValueAndStreamOutcome}; diff --git a/crate/cmd_model/src/cmd_execution_id.rs b/crate/cmd_model/src/cmd_execution_id.rs index ba80de775..372a18d31 100644 --- a/crate/cmd_model/src/cmd_execution_id.rs +++ b/crate/cmd_model/src/cmd_execution_id.rs @@ -1,10 +1,12 @@ use std::ops::Deref; +use serde::{Deserialize, Serialize}; + /// ID of a command execution. /// /// Uniqueness is not yet defined -- these may overlap with IDs from different /// machines. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] pub struct CmdExecutionId(u64); impl CmdExecutionId { diff --git a/crate/cmd_model/src/cmd_outcome.rs b/crate/cmd_model/src/cmd_outcome.rs index 643e7353d..6065ad7b6 100644 --- a/crate/cmd_model/src/cmd_outcome.rs +++ b/crate/cmd_model/src/cmd_outcome.rs @@ -1,6 +1,6 @@ use futures::Future; use indexmap::IndexMap; -use peace_cfg::ItemId; +use peace_core::ItemId; use crate::{CmdBlockDesc, ItemStreamOutcome}; diff --git a/crate/cmd_model/src/item_stream_outcome.rs b/crate/cmd_model/src/item_stream_outcome.rs index 86150b96c..ec5e997c6 100644 --- a/crate/cmd_model/src/item_stream_outcome.rs +++ b/crate/cmd_model/src/item_stream_outcome.rs @@ -1,5 +1,5 @@ use fn_graph::StreamOutcomeState; -use peace_cfg::ItemId; +use peace_core::ItemId; /// How a `Flow` stream operation ended and IDs that were processed. /// diff --git a/crate/cmd_model/src/stream_outcome_and_errors.rs b/crate/cmd_model/src/stream_outcome_and_errors.rs index 852ec4f3d..d2fc55684 100644 --- a/crate/cmd_model/src/stream_outcome_and_errors.rs +++ b/crate/cmd_model/src/stream_outcome_and_errors.rs @@ -1,6 +1,6 @@ use fn_graph::StreamOutcome; use indexmap::IndexMap; -use peace_cfg::ItemId; +use peace_core::ItemId; /// `CmdBlock` stream outcome and item wise errors. #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/crate/cmd_rt/Cargo.toml b/crate/cmd_rt/Cargo.toml index 2781f433c..d88118bcc 100644 --- a/crate/cmd_rt/Cargo.toml +++ b/crate/cmd_rt/Cargo.toml @@ -41,5 +41,6 @@ default = [] error_reporting = ["dep:miette"] output_progress = [ "peace_cfg/output_progress", + "peace_cmd_model/output_progress", "peace_rt_model/output_progress", ] diff --git a/crate/cmd_rt/src/cmd_block.rs b/crate/cmd_rt/src/cmd_block.rs index ef5640ca6..b5712938c 100644 --- a/crate/cmd_rt/src/cmd_block.rs +++ b/crate/cmd_rt/src/cmd_block.rs @@ -7,7 +7,7 @@ use peace_resource_rt::{resources::ts::SetUp, Resource, ResourceFetchError, Reso cfg_if::cfg_if! { if #[cfg(feature = "output_progress")] { - use peace_cfg::progress::CmdProgressUpdate; + use peace_cfg::progress::{CmdBlockItemInteractionType, CmdProgressUpdate}; use tokio::sync::mpsc::Sender; } } @@ -53,6 +53,11 @@ pub trait CmdBlock: Debug { /// Input type of the command block, e.g. `StatesCurrent`. type InputT: Resource + 'static; + /// Returns the type of interactions the `CmdBlock` has with + /// `ItemLocation`s. + #[cfg(feature = "output_progress")] + fn cmd_block_item_interaction_type(&self) -> CmdBlockItemInteractionType; + /// Fetch function for `InputT`. /// /// This is overridable so that `CmdBlock`s can change how their `InputT` is diff --git a/crate/cmd_rt/src/cmd_block/cmd_block_rt.rs b/crate/cmd_rt/src/cmd_block/cmd_block_rt.rs index 9139c301e..b8262db41 100644 --- a/crate/cmd_rt/src/cmd_block/cmd_block_rt.rs +++ b/crate/cmd_rt/src/cmd_block/cmd_block_rt.rs @@ -9,7 +9,7 @@ use crate::CmdBlockError; cfg_if::cfg_if! { if #[cfg(feature = "output_progress")] { - use peace_cfg::progress::CmdProgressUpdate; + use peace_cfg::progress::{CmdBlockItemInteractionType, CmdProgressUpdate}; use tokio::sync::mpsc::Sender; } } @@ -37,6 +37,11 @@ pub trait CmdBlockRt: Debug + Unpin { >, >; + /// Returns the type of interactions the `CmdBlock` has with + /// `ItemLocation`s. + #[cfg(feature = "output_progress")] + fn cmd_block_item_interaction_type(&self) -> CmdBlockItemInteractionType; + /// Returns the `String` representation of the `CmdBlock` in a /// `CmdExecution`. /// diff --git a/crate/cmd_rt/src/cmd_block/cmd_block_wrapper.rs b/crate/cmd_rt/src/cmd_block/cmd_block_wrapper.rs index 1d059dccb..d37acd664 100644 --- a/crate/cmd_rt/src/cmd_block/cmd_block_wrapper.rs +++ b/crate/cmd_rt/src/cmd_block/cmd_block_wrapper.rs @@ -12,7 +12,7 @@ use crate::{CmdBlock, CmdBlockError, CmdBlockRt}; cfg_if::cfg_if! { if #[cfg(feature = "output_progress")] { - use peace_cfg::progress::CmdProgressUpdate; + use peace_cfg::progress::{CmdBlockItemInteractionType, CmdProgressUpdate}; use tokio::sync::mpsc::Sender; } } @@ -161,6 +161,11 @@ where } } + #[cfg(feature = "output_progress")] + fn cmd_block_item_interaction_type(&self) -> CmdBlockItemInteractionType { + self.cmd_block.cmd_block_item_interaction_type() + } + fn cmd_block_desc(&self) -> CmdBlockDesc { let cmd_block_name = tynm::type_name_opts::(TypeParamsFmtOpts::Std); let cmd_block_input_names = self.cmd_block.input_type_names(); diff --git a/crate/cmd_rt/src/cmd_execution.rs b/crate/cmd_rt/src/cmd_execution.rs index db5865b87..6a703563c 100644 --- a/crate/cmd_rt/src/cmd_execution.rs +++ b/crate/cmd_rt/src/cmd_execution.rs @@ -249,6 +249,21 @@ where }); } + #[cfg(feature = "output_progress")] + { + let cmd_block_item_interaction_type = + cmd_block_rt.cmd_block_item_interaction_type(); + cmd_progress_tx + .send(CmdProgressUpdate::CmdBlockStart { + cmd_block_item_interaction_type, + }) + .await + .expect( + "Expected `CmdProgressUpdate` channel to remain open \ + while iterating over `CmdBlock`s.", + ); + } + let block_cmd_outcome_result = cmd_block_rt .exec( cmd_view, diff --git a/crate/cmd_rt/src/cmd_execution/cmd_execution_builder.rs b/crate/cmd_rt/src/cmd_execution/cmd_execution_builder.rs index 50d7111a2..71a49d0f4 100644 --- a/crate/cmd_rt/src/cmd_execution/cmd_execution_builder.rs +++ b/crate/cmd_rt/src/cmd_execution/cmd_execution_builder.rs @@ -121,8 +121,8 @@ where } } -impl<'types, ExecutionOutcome, CmdCtxTypesT> Default - for CmdExecutionBuilder<'types, ExecutionOutcome, CmdCtxTypesT> +impl Default + for CmdExecutionBuilder<'_, ExecutionOutcome, CmdCtxTypesT> where ExecutionOutcome: Debug + Resource + 'static, CmdCtxTypesT: CmdCtxTypesConstrained, diff --git a/crate/cmd_rt/src/progress.rs b/crate/cmd_rt/src/progress.rs index 5f120d45e..cf3cf8630 100644 --- a/crate/cmd_rt/src/progress.rs +++ b/crate/cmd_rt/src/progress.rs @@ -3,8 +3,8 @@ use std::ops::ControlFlow; use futures::stream::{self, StreamExt}; use peace_cfg::{ progress::{ - CmdProgressUpdate, ProgressDelta, ProgressMsgUpdate, ProgressStatus, ProgressTracker, - ProgressUpdate, ProgressUpdateAndId, + CmdBlockItemInteractionType, CmdProgressUpdate, ItemLocationState, ProgressDelta, + ProgressMsgUpdate, ProgressStatus, ProgressTracker, ProgressUpdate, ProgressUpdateAndId, }, ItemId, }; @@ -39,7 +39,14 @@ impl Progress { O: OutputWrite, { match cmd_progress_update { - CmdProgressUpdate::Item { + CmdProgressUpdate::CmdBlockStart { + cmd_block_item_interaction_type, + } => { + Self::handle_cmd_block_start(output, cmd_block_item_interaction_type).await; + + ControlFlow::Continue(()) + } + CmdProgressUpdate::ItemProgress { progress_update_and_id, } => { Self::handle_progress_update_and_id( @@ -51,6 +58,14 @@ impl Progress { ControlFlow::Continue(()) } + CmdProgressUpdate::ItemLocationState { + item_id, + item_location_state, + } => { + Self::handle_item_location_state(output, item_id, item_location_state).await; + + ControlFlow::Continue(()) + } CmdProgressUpdate::Interrupt => { stream::iter(progress_trackers.iter_mut()) .fold(output, |output, (item_id, progress_tracker)| async move { @@ -100,6 +115,29 @@ impl Progress { } } + async fn handle_cmd_block_start( + output: &mut O, + cmd_block_item_interaction_type: CmdBlockItemInteractionType, + ) where + O: OutputWrite, + { + output + .cmd_block_start(cmd_block_item_interaction_type) + .await; + } + + async fn handle_item_location_state( + output: &mut O, + item_id: ItemId, + item_location_state: ItemLocationState, + ) where + O: OutputWrite, + { + output + .item_location_state(item_id, item_location_state) + .await; + } + async fn handle_progress_update_and_id( output: &mut O, progress_trackers: &mut IndexMap, diff --git a/crate/code_gen/src/cmd/impl_build.rs b/crate/code_gen/src/cmd/impl_build.rs index 4be3a749b..736dea09a 100644 --- a/crate/code_gen/src/cmd/impl_build.rs +++ b/crate/code_gen/src/cmd/impl_build.rs @@ -45,7 +45,7 @@ pub fn impl_build(scope_struct: &ScopeStruct) -> proc_macro2::TokenStream { | ProfileSelection::FilterFunction ) | - // It doesn't make sense to have `profile_from_workpace_param` + // It doesn't make sense to have `profile_from_workspace_param` // when profile is none or multi. ( ProfileCount::Multiple, @@ -166,6 +166,7 @@ fn impl_build_for( let scope_fields = scope_fields(scope); let states_and_params_read_and_pg_init = states_and_params_read_and_pg_init(scope); let resources_insert = resources_insert(scope); + let states_example_insert = states_example_insert(scope); let scope_builder_deconstruct = scope_builder_deconstruct( scope_struct, @@ -324,7 +325,7 @@ fn impl_build_for( // .scope_builder // .workspace_params_selection // .0 - // .get(self.scope_builder.profile_selection.0) + // .get(&*self.scope_builder.profile_selection.0) // .cloned() // .ok_or(Error::WorkspaceParamsProfileNone)?; #profile_from_workspace @@ -502,6 +503,7 @@ fn impl_build_for( // output, // interruptibility, // workspace, + // resources: resources_override, // scope_builder: // #scope_builder_name { // profile_selection: ProfileSelected(profile) @@ -570,7 +572,7 @@ fn impl_build_for( // // === MultiProfileSingleFlow === // // { - // let (app_name, workspace_dirs, storage) = workspace.clone().into_inner(); + // let (app_name, workspace_dirs, storage) = (*workspace).clone().into_inner(); // let (workspace_dir, peace_dir, peace_app_dir) = workspace_dirs.into_inner(); // // resources.insert(app_name); @@ -582,7 +584,7 @@ fn impl_build_for( // } // === SingleProfileSingleFlow === // // { - // let (app_name, workspace_dirs, storage) = workspace.clone().into_inner(); + // let (app_name, workspace_dirs, storage) = (*workspace).clone().into_inner(); // let (workspace_dir, peace_dir, peace_app_dir) = workspace_dirs.into_inner(); // // resources.insert(app_name); @@ -599,8 +601,9 @@ fn impl_build_for( #resources_insert // === MultiProfileSingleFlow === // - // let flow_id = flow.flow_id(); - // let item_graph = flow.graph(); + // let flow_ref = &flow; + // let flow_id = flow_ref.flow_id(); + // let item_graph = flow_ref.graph(); // // let (params_specs_type_reg, states_type_reg) = // crate::ctx::cmd_ctx_builder::params_and_states_type_reg(item_graph); @@ -632,7 +635,7 @@ fn impl_build_for( // // so that multi-profile diffs can be done. // let params_specs = params_specs_stored.map(|params_specs_stored| { // crate::ctx::cmd_ctx_builder::params_specs_merge( - // &flow, + // flow_ref, // params_specs_provided, // Some(params_specs_stored), // ) @@ -683,7 +686,7 @@ fn impl_build_for( // .await?; // // // Call each `Item`'s initialization function. - // let resources = crate::ctx::cmd_ctx_builder::item_graph_setup( + // let mut resources = crate::ctx::cmd_ctx_builder::item_graph_setup( // item_graph, // resources // ) @@ -691,8 +694,9 @@ fn impl_build_for( // // === SingleProfileSingleFlow === // // // Set up resources for the flow's item graph - // let flow_id = flow.flow_id(); - // let item_graph = flow.graph(); + // let flow_ref = &flow; + // let flow_id = flow_ref.flow_id(); + // let item_graph = flow_ref.graph(); // // let (params_specs_type_reg, states_type_reg) = // crate::ctx::cmd_ctx_builder::params_and_states_type_reg(item_graph); @@ -712,7 +716,7 @@ fn impl_build_for( // .await?; // // let params_specs = crate::ctx::cmd_ctx_builder::params_specs_merge( - // &flow, + // flow_ref, // params_specs_provided, // params_specs_stored, // )?; @@ -742,7 +746,7 @@ fn impl_build_for( // } // // // Call each `Item`'s initialization function. - // let resources = crate::ctx::cmd_ctx_builder::item_graph_setup( + // let mut resources = crate::ctx::cmd_ctx_builder::item_graph_setup( // item_graph, // resources // ) @@ -770,6 +774,21 @@ fn impl_build_for( let params_type_regs = params_type_regs_builder.build(); + // Needs to come before `state_example`, because params resolution may need + // some resources to be inserted for `state_example` to work. + resources.merge(resources_override.into_inner()); + + // === SingleProfileSingleFlow === // + // // Fetching state example inserts it into resources. + // #[cfg(feature = "item_state_example")] + // { + // let () = flow.graph().iter().try_for_each(|item| { + // let _state_example = item.state_example(¶ms_specs, &resources)?; + // Ok::<_, AppError>(()) + // })?; + // } + #states_example_insert + let scope = #scope_type_path::new( // output, // interruptibility_state, @@ -924,6 +943,7 @@ fn scope_builder_deconstruct( output, interruptibility, workspace, + resources: resources_override, scope_builder: #scope_builder_name { // profile_selection: ProfileSelected(profile), // flow_selection: FlowSelected(flow), @@ -1305,7 +1325,7 @@ fn profile_from_workspace(profile_selection: ProfileSelection) -> proc_macro2::T .scope_builder .workspace_params_selection .0 - .get(self.scope_builder.profile_selection.0) + .get(&*self.scope_builder.profile_selection.0) .cloned() .ok_or(peace_rt_model::Error::WorkspaceParamsProfileNone)?; } @@ -1606,8 +1626,9 @@ fn states_and_params_read_and_pg_init(scope: Scope) -> proc_macro2::TokenStream // // These are then held in the scope for easy access for consumers. quote! { - let flow_id = flow.flow_id(); - let item_graph = flow.graph(); + let flow_ref = &flow; + let flow_id = flow_ref.flow_id(); + let item_graph = flow_ref.graph(); let (params_specs_type_reg, states_type_reg) = crate::ctx::cmd_ctx_builder::params_and_states_type_reg(item_graph); @@ -1639,7 +1660,7 @@ fn states_and_params_read_and_pg_init(scope: Scope) -> proc_macro2::TokenStream // so that multi-profile diffs can be done. let params_specs = params_specs_stored.map(|params_specs_stored| { crate::ctx::cmd_ctx_builder::params_specs_merge( - &flow, + flow_ref, params_specs_provided, Some(params_specs_stored), ) @@ -1690,7 +1711,7 @@ fn states_and_params_read_and_pg_init(scope: Scope) -> proc_macro2::TokenStream .await?; // Call each `Item`'s initialization function. - let resources = crate::ctx::cmd_ctx_builder::item_graph_setup( + let mut resources = crate::ctx::cmd_ctx_builder::item_graph_setup( item_graph, resources ) @@ -1716,8 +1737,9 @@ fn states_and_params_read_and_pg_init(scope: Scope) -> proc_macro2::TokenStream // It also requires multiple item graph setups to work without conflicting // with each other. quote! { - let flow_id = flow.flow_id(); - let item_graph = flow.graph(); + let flow_ref = &flow; + let flow_id = flow_ref.flow_id(); + let item_graph = flow_ref.graph(); let (params_specs_type_reg, states_type_reg) = crate::ctx::cmd_ctx_builder::params_and_states_type_reg(item_graph); @@ -1737,7 +1759,7 @@ fn states_and_params_read_and_pg_init(scope: Scope) -> proc_macro2::TokenStream .await?; let params_specs = crate::ctx::cmd_ctx_builder::params_specs_merge( - &flow, + flow_ref, params_specs_provided, params_specs_stored, )?; @@ -1767,7 +1789,7 @@ fn states_and_params_read_and_pg_init(scope: Scope) -> proc_macro2::TokenStream } // Call each `Item`'s initialization function. - let resources = crate::ctx::cmd_ctx_builder::item_graph_setup( + let mut resources = crate::ctx::cmd_ctx_builder::item_graph_setup( item_graph, resources ) @@ -1801,7 +1823,7 @@ fn resources_insert(scope: Scope) -> proc_macro2::TokenStream { Scope::MultiProfileSingleFlow => { quote! { { - let (app_name, workspace_dirs, storage) = workspace.clone().into_inner(); + let (app_name, workspace_dirs, storage) = (*workspace).clone().into_inner(); let (workspace_dir, peace_dir, peace_app_dir) = workspace_dirs.into_inner(); resources.insert(app_name); @@ -1816,7 +1838,7 @@ fn resources_insert(scope: Scope) -> proc_macro2::TokenStream { Scope::SingleProfileSingleFlow => { quote! { { - let (app_name, workspace_dirs, storage) = workspace.clone().into_inner(); + let (app_name, workspace_dirs, storage) = (*workspace).clone().into_inner(); let (workspace_dir, peace_dir, peace_app_dir) = workspace_dirs.into_inner(); resources.insert(app_name); @@ -1837,3 +1859,24 @@ fn resources_insert(scope: Scope) -> proc_macro2::TokenStream { } } } + +fn states_example_insert(scope: Scope) -> proc_macro2::TokenStream { + match scope { + Scope::SingleProfileSingleFlow => { + quote! { + // Fetching state example inserts it into resources. + #[cfg(feature = "item_state_example")] + { + let () = flow.graph().iter().try_for_each(|item| { + let _state_example = item.state_example(¶ms_specs, &resources)?; + Ok::<_, AppError>(()) + })?; + } + } + } + Scope::MultiProfileSingleFlow + | Scope::MultiProfileNoFlow + | Scope::NoProfileNoFlow + | Scope::SingleProfileNoFlow => proc_macro2::TokenStream::new(), + } +} diff --git a/crate/code_gen/src/cmd/impl_common_fns.rs b/crate/code_gen/src/cmd/impl_common_fns.rs index ad58c50fb..d729ca690 100644 --- a/crate/code_gen/src/cmd/impl_common_fns.rs +++ b/crate/code_gen/src/cmd/impl_common_fns.rs @@ -20,6 +20,7 @@ pub fn impl_common_fns(scope_struct: &ScopeStruct) -> proc_macro2::TokenStream { output, interruptibility: _, workspace, + resources, scope_builder, } = self; @@ -27,9 +28,21 @@ pub fn impl_common_fns(scope_struct: &ScopeStruct) -> proc_macro2::TokenStream { output, interruptibility, workspace, + resources, scope_builder, } } + + /// Sets the interrupt receiver and strategy so `CmdExecution`s can be interrupted. + pub fn with_resource( + mut self, + resource: R, + ) -> Self + where R: peace_resource_rt::Resource + { + self.resources.insert(resource); + self + } }; if scope.flow_count() == FlowCount::One { diff --git a/crate/code_gen/src/cmd/impl_constructor.rs b/crate/code_gen/src/cmd/impl_constructor.rs index cba9a788c..9a9620598 100644 --- a/crate/code_gen/src/cmd/impl_constructor.rs +++ b/crate/code_gen/src/cmd/impl_constructor.rs @@ -101,6 +101,7 @@ pub fn impl_constructor(scope_struct: &ScopeStruct) -> proc_macro2::TokenStream output, interruptibility: interruptible::Interruptibility::NonInterruptible, workspace, + resources: peace_resource_rt::Resources::new(), scope_builder, } } diff --git a/crate/code_gen/src/cmd/impl_with_flow.rs b/crate/code_gen/src/cmd/impl_with_flow.rs index 3eaf0056c..79d6a1a98 100644 --- a/crate/code_gen/src/cmd/impl_with_flow.rs +++ b/crate/code_gen/src/cmd/impl_with_flow.rs @@ -55,7 +55,7 @@ pub fn impl_with_flow(scope_struct: &ScopeStruct) -> proc_macro2::TokenStream { { pub fn with_flow( self, - flow: &'ctx peace_rt_model::Flow, + flow: own::OwnedOrRef<'ctx, peace_rt_model::Flow>, ) -> // ```rust,ignore // crate::ctx::CmdCtxBuilder< @@ -82,6 +82,7 @@ pub fn impl_with_flow(scope_struct: &ScopeStruct) -> proc_macro2::TokenStream { output, interruptibility, workspace, + resources, scope_builder: #scope_builder_name { // profile_selection, @@ -110,6 +111,7 @@ pub fn impl_with_flow(scope_struct: &ScopeStruct) -> proc_macro2::TokenStream { output, interruptibility, workspace, + resources, scope_builder, } } diff --git a/crate/code_gen/src/cmd/impl_with_param.rs b/crate/code_gen/src/cmd/impl_with_param.rs index 87c27deb4..4a6c03fea 100644 --- a/crate/code_gen/src/cmd/impl_with_param.rs +++ b/crate/code_gen/src/cmd/impl_with_param.rs @@ -188,6 +188,7 @@ fn impl_with_param_key_unknown( output, interruptibility, workspace, + resources, scope_builder: #scope_builder_name { // profile_selection, @@ -231,6 +232,7 @@ fn impl_with_param_key_unknown( output, interruptibility, workspace, + resources, scope_builder, } } @@ -346,6 +348,7 @@ fn impl_with_param_key_known( output, interruptibility, workspace, + resources, scope_builder: #scope_builder_name { // profile_selection, @@ -385,6 +388,7 @@ fn impl_with_param_key_known( output, interruptibility, workspace, + resources, scope_builder, } } diff --git a/crate/code_gen/src/cmd/impl_with_params_k.rs b/crate/code_gen/src/cmd/impl_with_params_k.rs index b27f83fcc..be8ccece1 100644 --- a/crate/code_gen/src/cmd/impl_with_params_k.rs +++ b/crate/code_gen/src/cmd/impl_with_params_k.rs @@ -173,6 +173,7 @@ fn impl_with_params_k_key_unknown( output, interruptibility, workspace, + resources, scope_builder: #scope_builder_name { // profile_selection, @@ -209,6 +210,7 @@ fn impl_with_params_k_key_unknown( output, interruptibility, workspace, + resources, scope_builder, } } @@ -283,6 +285,7 @@ fn impl_with_param_key_known( output, interruptibility, workspace, + resources, scope_builder: #scope_builder_name { // profile_selection, @@ -315,6 +318,7 @@ fn impl_with_param_key_known( output, interruptibility, workspace, + resources, scope_builder, } } diff --git a/crate/code_gen/src/cmd/impl_with_profile.rs b/crate/code_gen/src/cmd/impl_with_profile.rs index 99db11ba7..3ec19ba9f 100644 --- a/crate/code_gen/src/cmd/impl_with_profile.rs +++ b/crate/code_gen/src/cmd/impl_with_profile.rs @@ -79,6 +79,7 @@ pub fn impl_with_profile(scope_struct: &ScopeStruct) -> proc_macro2::TokenStream output, interruptibility, workspace, + resources, scope_builder: #scope_builder_name { // profile_selection: ProfileNotSelected, @@ -107,6 +108,7 @@ pub fn impl_with_profile(scope_struct: &ScopeStruct) -> proc_macro2::TokenStream output, interruptibility, workspace, + resources, scope_builder, } } @@ -182,7 +184,7 @@ pub fn impl_with_profile_from_workspace_param( { pub fn with_profile_from_workspace_param<'key>( self, - workspace_param_k: &'key WorkspaceParamsK, + workspace_param_k: own::OwnedOrRef<'key, WorkspaceParamsK>, ) -> // crate::ctx::CmdCtxBuilder< // 'ctx, @@ -207,6 +209,7 @@ pub fn impl_with_profile_from_workspace_param( output, interruptibility, workspace, + resources, scope_builder: #scope_builder_name { // profile_selection: ProfileNotSelected, @@ -235,6 +238,7 @@ pub fn impl_with_profile_from_workspace_param( output, interruptibility, workspace, + resources, scope_builder, } } diff --git a/crate/code_gen/src/cmd/impl_with_profile_filter.rs b/crate/code_gen/src/cmd/impl_with_profile_filter.rs index 43330c05f..1d312c10c 100644 --- a/crate/code_gen/src/cmd/impl_with_profile_filter.rs +++ b/crate/code_gen/src/cmd/impl_with_profile_filter.rs @@ -83,6 +83,7 @@ pub fn impl_with_profile_filter(scope_struct: &ScopeStruct) -> proc_macro2::Toke output, interruptibility, workspace, + resources, scope_builder: #scope_builder_name { // profile_selection: ProfileNotSelected, @@ -111,6 +112,7 @@ pub fn impl_with_profile_filter(scope_struct: &ScopeStruct) -> proc_macro2::Toke output, interruptibility, workspace, + resources, scope_builder, } } diff --git a/crate/core/src/lib.rs b/crate/core/src/lib.rs index fe1cc0cdf..aeaf91e02 100644 --- a/crate/core/src/lib.rs +++ b/crate/core/src/lib.rs @@ -79,7 +79,7 @@ macro_rules! id_newtype { /// compile time checks and returns a `const` value. /// #[doc = concat!("[`", stringify!($macro_name), "!`]: peace_static_check_macros::profile")] - pub fn new(s: &'static str) -> Result { + pub fn new(s: &'static str) -> Result> { Self::try_from(s) } @@ -106,6 +106,16 @@ macro_rules! id_newtype { first_char_valid && remainder_chars_valid } + + /// Returns the inner `Cow<'static, str>`. + pub fn into_inner(self) -> Cow<'static, str> { + self.0 + } + + /// Returns the `&str` held by this ID. + pub fn as_str(&self) -> &str { + &self.0 + } } impl std::ops::Deref for $ty_name { diff --git a/crate/core/src/progress.rs b/crate/core/src/progress.rs index 47b241892..20dbb69d2 100644 --- a/crate/core/src/progress.rs +++ b/crate/core/src/progress.rs @@ -1,12 +1,16 @@ pub use self::{ - cmd_progress_update::CmdProgressUpdate, progress_complete::ProgressComplete, - progress_delta::ProgressDelta, progress_limit::ProgressLimit, - progress_msg_update::ProgressMsgUpdate, progress_sender::ProgressSender, - progress_status::ProgressStatus, progress_tracker::ProgressTracker, - progress_update::ProgressUpdate, progress_update_and_id::ProgressUpdateAndId, + cmd_block_item_interaction_type::CmdBlockItemInteractionType, + cmd_progress_update::CmdProgressUpdate, item_location_state::ItemLocationState, + progress_complete::ProgressComplete, progress_delta::ProgressDelta, + progress_limit::ProgressLimit, progress_msg_update::ProgressMsgUpdate, + progress_sender::ProgressSender, progress_status::ProgressStatus, + progress_tracker::ProgressTracker, progress_update::ProgressUpdate, + progress_update_and_id::ProgressUpdateAndId, }; +mod cmd_block_item_interaction_type; mod cmd_progress_update; +mod item_location_state; mod progress_complete; mod progress_delta; mod progress_limit; diff --git a/crate/core/src/progress/cmd_block_item_interaction_type.rs b/crate/core/src/progress/cmd_block_item_interaction_type.rs new file mode 100644 index 000000000..17dd71afd --- /dev/null +++ b/crate/core/src/progress/cmd_block_item_interaction_type.rs @@ -0,0 +1,20 @@ +use serde::{Deserialize, Serialize}; + +/// Type of interactions that a `CmdBlock`s has with `ItemLocation`s. +/// +/// # Design +/// +/// Together with `ProgressStatus` and `ItemLocationState`, this is used to +/// compute how an `ItemLocation` should be rendered to a user. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub enum CmdBlockItemInteractionType { + /// Creates / modifies / deletes the item. + /// + /// Makes write calls to `ItemLocation`s. + Write, + /// Makes read-only calls to `ItemLocation`s. + Read, + /// Local logic that does not interact with `ItemLocation`s / external + /// services. + Local, +} diff --git a/crate/core/src/progress/cmd_progress_update.rs b/crate/core/src/progress/cmd_progress_update.rs index 4f7af6c8b..c92c15cc9 100644 --- a/crate/core/src/progress/cmd_progress_update.rs +++ b/crate/core/src/progress/cmd_progress_update.rs @@ -1,22 +1,43 @@ use serde::{Deserialize, Serialize}; -use crate::progress::ProgressUpdateAndId; +use crate::{ + progress::{CmdBlockItemInteractionType, ItemLocationState, ProgressUpdateAndId}, + ItemId, +}; /// Progress update that affects all `ProgressTracker`s. /// /// This is sent at the `CmdExecution` level. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub enum CmdProgressUpdate { + /// A `CmdBlock` has started. + CmdBlockStart { + /// The type of interactions the `CmdBlock` has with the + /// `ItemLocation`s. + cmd_block_item_interaction_type: CmdBlockItemInteractionType, + }, /// `ProgressUpdateAndId` for a single item. /// /// # Design Note /// /// This isn't a tuple newtype as `serde_yaml` `0.9` is unable to serialize /// newtype enum variants. - Item { + ItemProgress { /// The update. progress_update_and_id: ProgressUpdateAndId, }, + /// `ItemLocationState` for a single item. + /// + /// # Design Note + /// + /// `ItemLocationState` should live in `peace_item_model`, but this creates + /// a circular dependency. + ItemLocationState { + /// ID of the `Item`. + item_id: ItemId, + /// The representation of the state of an `ItemLocation`. + item_location_state: ItemLocationState, + }, /// `CmdExecution` has been interrupted, we should indicate this on all /// unstarted progress bars. Interrupt, @@ -26,7 +47,7 @@ pub enum CmdProgressUpdate { impl From for CmdProgressUpdate { fn from(progress_update_and_id: ProgressUpdateAndId) -> Self { - Self::Item { + Self::ItemProgress { progress_update_and_id, } } diff --git a/crate/core/src/progress/item_location_state.rs b/crate/core/src/progress/item_location_state.rs new file mode 100644 index 000000000..db5c5010f --- /dev/null +++ b/crate/core/src/progress/item_location_state.rs @@ -0,0 +1,23 @@ +use serde::{Deserialize, Serialize}; + +/// A low-resolution representation of the state of an [`ItemLocation`]. +/// +/// Combined with [`ProgressStatus`], [`ItemLocationStateInProgress`] can be +/// computed, to determine how an [`ItemLocation`] should be rendered. +/// +/// [`ItemLocation`]: crate::ItemLocation +/// [`ItemLocationStateInProgress`]: crate::ItemLocationStateInProgress +/// [`ProgressStatus`]: peace_core::progress::ProgressStatus +#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub enum ItemLocationState { + /// [`ItemLocation`] does not exist. + /// + /// This means it should be rendered invisible / low opacity. + NotExists, + /// [`ItemLocation`] exists. + /// + /// This means it should be rendered with full opacity. + /// + /// [`ItemLocation`]: crate::ItemLocation + Exists, +} diff --git a/crate/core/src/progress/progress_sender.rs b/crate/core/src/progress/progress_sender.rs index ba228306b..87cb93cbe 100644 --- a/crate/core/src/progress/progress_sender.rs +++ b/crate/core/src/progress/progress_sender.rs @@ -2,7 +2,8 @@ use tokio::sync::mpsc::Sender; use crate::{ progress::{ - CmdProgressUpdate, ProgressDelta, ProgressMsgUpdate, ProgressUpdate, ProgressUpdateAndId, + CmdProgressUpdate, ItemLocationState, ProgressDelta, ProgressMsgUpdate, ProgressUpdate, + ProgressUpdateAndId, }, ItemId, }; @@ -79,4 +80,22 @@ impl<'exec> ProgressSender<'exec> { .into(), ); } + + /// Sends an `ItemLocationState` update. + /// + /// # Implementors + /// + /// This is only intended for use by the Peace framework for rendering. + /// + /// # Maintainers + /// + /// This is used in `ItemWrapper`. + pub fn item_location_state_send(&self, item_location_state: ItemLocationState) { + let _progress_send_unused = + self.progress_tx + .try_send(CmdProgressUpdate::ItemLocationState { + item_id: self.item_id.clone(), + item_location_state, + }); + } } diff --git a/crate/data/Cargo.toml b/crate/data/Cargo.toml index 1bdf99108..e844d9498 100644 --- a/crate/data/Cargo.toml +++ b/crate/data/Cargo.toml @@ -24,3 +24,7 @@ fn_graph = { workspace = true } peace_core = { workspace = true } peace_data_derive = { workspace = true } serde = { workspace = true, features = ["derive"] } + +[features] +default = [] +item_state_example = [] diff --git a/crate/data/src/accessors/r_maybe.rs b/crate/data/src/accessors/r_maybe.rs index e21ea9a1f..3f81b9210 100644 --- a/crate/data/src/accessors/r_maybe.rs +++ b/crate/data/src/accessors/r_maybe.rs @@ -56,7 +56,7 @@ where } } -impl<'borrow, T> DataAccess for RMaybe<'borrow, T> +impl DataAccess for RMaybe<'_, T> where T: Debug + Send + Sync + 'static, { @@ -77,7 +77,7 @@ where } } -impl<'borrow, T> DataAccessDyn for RMaybe<'borrow, T> +impl DataAccessDyn for RMaybe<'_, T> where T: Debug + Send + Sync + 'static, { diff --git a/crate/data/src/accessors/w_maybe.rs b/crate/data/src/accessors/w_maybe.rs index 13d06d8c3..e9a681fab 100644 --- a/crate/data/src/accessors/w_maybe.rs +++ b/crate/data/src/accessors/w_maybe.rs @@ -38,7 +38,7 @@ where } } -impl<'borrow, T> std::ops::DerefMut for WMaybe<'borrow, T> +impl std::ops::DerefMut for WMaybe<'_, T> where T: Debug + Send + Sync + 'static, { @@ -65,7 +65,7 @@ where } } -impl<'borrow, T> DataAccess for WMaybe<'borrow, T> +impl DataAccess for WMaybe<'_, T> where T: Debug + Send + Sync + 'static, { @@ -86,7 +86,7 @@ where } } -impl<'borrow, T> DataAccessDyn for WMaybe<'borrow, T> +impl DataAccessDyn for WMaybe<'_, T> where T: Debug + Send + Sync + 'static, { diff --git a/crate/data/src/marker.rs b/crate/data/src/marker.rs index 25ce33224..2b913d305 100644 --- a/crate/data/src/marker.rs +++ b/crate/data/src/marker.rs @@ -9,7 +9,13 @@ // Remember to update there when updating here. pub use self::{apply_dry::ApplyDry, clean::Clean, current::Current, goal::Goal}; +#[cfg(feature = "item_state_example")] +pub use self::example::Example; + mod apply_dry; mod clean; mod current; mod goal; + +#[cfg(feature = "item_state_example")] +mod example; diff --git a/crate/data/src/marker/example.rs b/crate/data/src/marker/example.rs new file mode 100644 index 000000000..d1ac5aa2e --- /dev/null +++ b/crate/data/src/marker/example.rs @@ -0,0 +1,30 @@ +use serde::{Deserialize, Serialize}; + +/// Marker for example state. +/// +/// This is used for referential param values, where an item param value is +/// dependent on the state of a predecessor's state. +/// +/// An `Example` is set to `Some` whenever an item's example state +/// is discovered. This is used for rendering outcome diagrams in the following +/// cases: +/// +/// 1. Rendering an example fully deployed state. +/// 2. Rendering invisible placeholder nodes and edges, so that the layout of a +/// diagram is consistent as more items are applied. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct Example(pub Option); + +impl std::ops::Deref for Example { + type Target = Option; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for Example { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/crate/flow_model/Cargo.toml b/crate/flow_model/Cargo.toml index 966ae52c9..39d5934f5 100644 --- a/crate/flow_model/Cargo.toml +++ b/crate/flow_model/Cargo.toml @@ -21,3 +21,9 @@ fn_graph = { workspace = true, features = ["graph_info"] } peace_core = { workspace = true } serde = { workspace = true, features = ["derive"] } tynm = { workspace = true, features = ["info", "serde"] } + +[features] +default = [] +output_progress = [ + "peace_core/output_progress", +] diff --git a/crate/flow_model/src/flow_spec_info.rs b/crate/flow_model/src/flow_spec_info.rs index f630b52d2..69532d545 100644 --- a/crate/flow_model/src/flow_spec_info.rs +++ b/crate/flow_model/src/flow_spec_info.rs @@ -1,20 +1,31 @@ -use std::collections::HashSet; - use dot_ix::model::{ common::{EdgeId, Edges, NodeHierarchy, NodeId, NodeNames}, - info_graph::{GraphDir, InfoGraph}, + info_graph::{GraphDir, GraphStyle, InfoGraph}, }; -use fn_graph::{daggy::Walker, Edge, FnId, GraphInfo}; +use fn_graph::{daggy::Walker, Edge, GraphInfo}; use peace_core::FlowId; - use serde::{Deserialize, Serialize}; use crate::ItemSpecInfo; +#[cfg(feature = "output_progress")] +use std::collections::HashMap; + +#[cfg(feature = "output_progress")] +use dot_ix::model::{ + common::AnyId, + theme::{AnyIdOrDefaults, CssClassPartials, Theme, ThemeAttr}, +}; +#[cfg(feature = "output_progress")] +use peace_core::{ + progress::{ProgressComplete, ProgressStatus}, + ItemId, +}; + /// 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)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct FlowSpecInfo { /// ID of the flow. pub flow_id: FlowId, @@ -34,6 +45,29 @@ impl FlowSpecInfo { /// Returns an [`InfoGraph`] that represents the progress of the flow's /// execution. pub fn to_progress_info_graph(&self) -> InfoGraph { + self.to_progress_info_graph_internal( + #[cfg(feature = "output_progress")] + &HashMap::new(), + ) + } + + /// Returns an [`InfoGraph`] that represents the progress of the flow's + /// execution. + #[cfg(feature = "output_progress")] + pub fn to_progress_info_graph_with_statuses( + &self, + item_progress_statuses: &HashMap, + ) -> InfoGraph { + self.to_progress_info_graph_internal(item_progress_statuses) + } + + fn to_progress_info_graph_internal( + &self, + #[cfg(feature = "output_progress")] item_progress_statuses: &HashMap< + ItemId, + ProgressStatus, + >, + ) -> InfoGraph { let graph_info = &self.graph_info; let item_count = graph_info.node_count(); @@ -50,151 +84,89 @@ impl FlowSpecInfo { let edges = progress_node_edges(graph_info); let node_names = node_names(graph_info); - InfoGraph::builder() + let info_graph = InfoGraph::default() + .with_graph_style(GraphStyle::Circle) .with_direction(GraphDir::Vertical) .with_hierarchy(hierarchy) .with_edges(edges) - .with_node_names(node_names) - .build() - } - - /// Returns an [`InfoGraph`] that represents the outcome of the flow's - /// execution. - pub fn to_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_names = node_names(graph_info); + .with_node_names(node_names); + + #[cfg(feature = "output_progress")] + { + if !item_progress_statuses.is_empty() { + let theme = graph_info.iter_insertion_with_indices().fold( + Theme::new(), + |mut theme, (_node_index, item_spec_info)| { + let item_id = &item_spec_info.item_id; + + if let Some(progress_status) = item_progress_statuses.get(item_id) { + let css_class_partials = + item_progress_css_class_partials(progress_status); + + let any_id = AnyId::try_from(item_id.as_str().to_string()).expect( + "Expected `peace` `ItemId`s to be valid `dot_ix` `AnyId`s.`", + ); + + if !css_class_partials.is_empty() { + theme + .styles + .insert(AnyIdOrDefaults::AnyId(any_id), css_class_partials); + } + } + theme + }, + ); - InfoGraph::builder() - .with_direction(GraphDir::Vertical) - .with_hierarchy(hierarchy) - .with_edges(edges) - .with_node_names(node_names) - .build() - } + return info_graph.with_theme(theme).with_css(String::from( + r#" +@keyframes ellipse-stroke-dashoffset-move { + 0% { stroke-dashoffset: 30; } + 100% { stroke-dashoffset: 0; } } - -/// 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. - // - // We actually want to determine nesting from the following information: - // - // * Host machines: - // - // A file transfer would have a source host, source path, dest host, dest - // path, and these exist in the same Item's parameters. - // - // * Physical nesting: - // - // - Configuration that lives inside a server. - // - Cloud resource that lives inside a subnet. - // - // Should this be provided by the item or tool developer? - // - // Probably the item. The item knows its parameters (which may be mapped - // from other items' state), so the containment is returned based on the - // item knowing its parent container is from a source / destination - // parameter. - 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[child_node_index]; - hierarchy.insert( - item_spec_info_to_node_id(item_spec_info), - child_node_hierarchy, - ); - } - }); + } - Some(hierarchy) + info_graph + } } -/// Returns the list of edges between items in the graph. -fn outcome_node_edges(graph_info: &GraphInfo) -> Edges { - graph_info.iter_insertion_with_indices().fold( - Edges::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!("{item_id}__{child_item_id}")).expect( - "Expected `peace` `ItemId`s concatenated \ - to be valid `dot_ix` `EdgeId`s.", - ), - [item_id, child_item_id], - ); - }); +#[cfg(feature = "output_progress")] +fn item_progress_css_class_partials(progress_status: &ProgressStatus) -> CssClassPartials { + let mut css_class_partials = CssClassPartials::with_capacity(4); - edges - }, - ) + match progress_status { + ProgressStatus::Initialized => {} + ProgressStatus::Interrupted => { + css_class_partials.insert(ThemeAttr::ShapeColor, "yellow".to_string()); + } + ProgressStatus::ExecPending | ProgressStatus::Queued => { + css_class_partials.insert(ThemeAttr::ShapeColor, "indigo".to_string()); + } + ProgressStatus::Running => { + css_class_partials.insert(ThemeAttr::StrokeStyle, "dashed".to_string()); + css_class_partials.insert(ThemeAttr::StrokeWidth, "[2px]".to_string()); + css_class_partials.insert(ThemeAttr::ShapeColor, "blue".to_string()); + css_class_partials.insert( + ThemeAttr::Animate, + "[ellipse-stroke-dashoffset-move_1s_linear_infinite]".to_string(), + ); + } + ProgressStatus::RunningStalled => { + css_class_partials.insert(ThemeAttr::ShapeColor, "amber".to_string()); + } + ProgressStatus::UserPending => { + css_class_partials.insert(ThemeAttr::ShapeColor, "purple".to_string()); + } + ProgressStatus::Complete(ProgressComplete::Success) => { + css_class_partials.insert(ThemeAttr::ShapeColor, "green".to_string()); + } + ProgressStatus::Complete(ProgressComplete::Fail) => { + css_class_partials.insert(ThemeAttr::ShapeColor, "red".to_string()); + } + } + css_class_partials } /// Returns the list of edges between items in the graph for progress. @@ -230,13 +202,10 @@ fn progress_node_edges(graph_info: &GraphInfo) -> Edges { .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!("{item_id}__{child_item_id}")).expect( - "Expected `peace` `ItemId`s concatenated \ - to be valid `dot_ix` `EdgeId`s.", - ), - [item_id, child_item_id], + let edge_id = EdgeId::try_from(format!("{item_id}__{child_item_id}")).expect( + "Expected concatenated `peace` `ItemId`s to be valid `dot_ix` `EdgeId`s.", ); + edges.insert(edge_id, [item_id, child_item_id]); }); edges diff --git a/crate/fmt/src/presentable/code_inline.rs b/crate/fmt/src/presentable/code_inline.rs index 384632555..61b17f49a 100644 --- a/crate/fmt/src/presentable/code_inline.rs +++ b/crate/fmt/src/presentable/code_inline.rs @@ -16,7 +16,7 @@ impl<'s> CodeInline<'s> { } #[async_trait::async_trait(?Send)] -impl<'s> Presentable for CodeInline<'s> { +impl Presentable for CodeInline<'_> { async fn present<'output, PR>(&self, presenter: &mut PR) -> Result<(), PR::Error> where PR: Presenter<'output>, diff --git a/crate/fmt/src/presentable/tuple_impl.rs b/crate/fmt/src/presentable/tuple_impl.rs index 87881606d..d74d28509 100644 --- a/crate/fmt/src/presentable/tuple_impl.rs +++ b/crate/fmt/src/presentable/tuple_impl.rs @@ -64,14 +64,10 @@ tuple_presentable_impl!( [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13] ); tuple_presentable_impl!( - ( - T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14 - ), + (T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] ); tuple_presentable_impl!( - ( - T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15 - ), + (T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] ); diff --git a/crate/item_model/Cargo.toml b/crate/item_model/Cargo.toml new file mode 100644 index 000000000..1b67b7cb5 --- /dev/null +++ b/crate/item_model/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "peace_item_model" +description = "Data types for resource interactions for the Peace framework." +documentation = "https://docs.rs/peace_item_model/" +authors.workspace = true +version.workspace = true +edition.workspace = true +repository.workspace = true +homepage.workspace = true +readme.workspace = true +categories.workspace = true +keywords.workspace = true +license.workspace = true + +[lints] +workspace = true + +[lib] +doctest = false +test = false + +[dependencies] +indexmap = { workspace = true, optional = true, features = ["serde"] } +peace_core = { workspace = true, optional = true } +peace_cmd_model = { workspace = true, optional = true } +serde = { workspace = true, features = ["derive"] } +url = { workspace = true, features = ["serde"] } + +[features] +default = [] +item_locations_and_interactions = [ + "dep:indexmap", + "dep:peace_core", +] +output_progress = [ + "dep:peace_core", + "dep:peace_cmd_model", + "peace_core/output_progress", + "peace_cmd_model/output_progress", +] diff --git a/crate/item_model/src/item_interaction.rs b/crate/item_model/src/item_interaction.rs new file mode 100644 index 000000000..5fe084076 --- /dev/null +++ b/crate/item_model/src/item_interaction.rs @@ -0,0 +1,50 @@ +use serde::{Deserialize, Serialize}; + +mod item_interaction_pull; +mod item_interaction_push; +mod item_interaction_within; + +pub use self::{ + item_interaction_pull::ItemInteractionPull, item_interaction_push::ItemInteractionPush, + item_interaction_within::ItemInteractionWithin, +}; + +/// Represents the resources that are read from / written to. +/// +/// This is used on an outcome diagram to highlight the resources that are being +/// accessed. For example, a file is read from the user's computer, and uploaded +/// / written to a file server. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub enum ItemInteraction { + /// Represents a location-to-location push interaction. + /// + /// This can represent a file transfer from one host to another. + Push(ItemInteractionPush), + /// Represents a location-to-location pull interaction. + /// + /// This can represent a file download from a server. + Pull(ItemInteractionPull), + /// Represents a resource interaction that happens within a location. + /// + /// This can represent application installation / startup happening on a + /// server. + Within(ItemInteractionWithin), +} + +impl From for ItemInteraction { + fn from(item_interaction_push: ItemInteractionPush) -> Self { + Self::Push(item_interaction_push) + } +} + +impl From for ItemInteraction { + fn from(item_interaction_pull: ItemInteractionPull) -> Self { + Self::Pull(item_interaction_pull) + } +} + +impl From for ItemInteraction { + fn from(item_interaction_within: ItemInteractionWithin) -> Self { + Self::Within(item_interaction_within) + } +} diff --git a/crate/item_model/src/item_interaction/item_interaction_pull.rs b/crate/item_model/src/item_interaction/item_interaction_pull.rs new file mode 100644 index 000000000..c9901d37b --- /dev/null +++ b/crate/item_model/src/item_interaction/item_interaction_pull.rs @@ -0,0 +1,59 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ItemLocation, ItemLocationAncestors}; + +/// Represents a location-to-location pull interaction. +/// +/// This can represent a file download from a server. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct ItemInteractionPull { + /// Where the interaction begins from. + /// + /// Example: + /// + /// 1. `ItemLocation::localhost()` + /// 2. `ItemLocation::new("/path/to/file", ItemLocationType::Path)` + pub location_client: ItemLocationAncestors, + /// Where the interaction goes to. + /// + /// Example: + /// + /// 1. `ItemLocation::new("app.domain.com", ItemLocationType::Host)` + /// 2. `ItemLocation::new("http://app.domain.com/resource", + /// ItemLocationType::Path)` + pub location_server: ItemLocationAncestors, +} + +impl ItemInteractionPull { + /// Returns a new `ItemInteractionPull`. + pub fn new( + location_client: ItemLocationAncestors, + location_server: ItemLocationAncestors, + ) -> Self { + Self { + location_client, + location_server, + } + } + + /// Returns where the interaction begins from. + /// + /// Example: + /// + /// 1. `ItemLocation::localhost()` + /// 2. `ItemLocation::new("/path/to/file", ItemLocationType::Path)` + pub fn location_client(&self) -> &[ItemLocation] { + &self.location_client + } + + /// Returns where the interaction goes to. + /// + /// Example: + /// + /// 1. `ItemLocation::new("app.domain.com", ItemLocationType::Host)` + /// 2. `ItemLocation::new("http://app.domain.com/resource", + /// ItemLocationType::Path)` + pub fn location_server(&self) -> &[ItemLocation] { + &self.location_server + } +} diff --git a/crate/item_model/src/item_interaction/item_interaction_push.rs b/crate/item_model/src/item_interaction/item_interaction_push.rs new file mode 100644 index 000000000..12e48f733 --- /dev/null +++ b/crate/item_model/src/item_interaction/item_interaction_push.rs @@ -0,0 +1,56 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ItemLocation, ItemLocationAncestors}; + +/// Represents a location-to-location push interaction. +/// +/// This can represent a file transfer from one host to another. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct ItemInteractionPush { + /// Where the interaction begins from. + /// + /// Example: + /// + /// 1. `ItemLocation::localhost()` + /// 2. `ItemLocation::new("/path/to/file", ItemLocationType::Path)` + pub location_from: ItemLocationAncestors, + /// Where the interaction goes to. + /// + /// Example: + /// + /// 1. `ItemLocation::new("app.domain.com", ItemLocationType::Host)` + /// 2. `ItemLocation::new("http://app.domain.com/resource", + /// ItemLocationType::Path)` + pub location_to: ItemLocationAncestors, +} + +impl ItemInteractionPush { + /// Returns a new `ItemInteractionPush`. + pub fn new(location_from: ItemLocationAncestors, location_to: ItemLocationAncestors) -> Self { + Self { + location_from, + location_to, + } + } + + /// Returns where the interaction begins from. + /// + /// Example: + /// + /// 1. `ItemLocation::localhost()` + /// 2. `ItemLocation::new("/path/to/file", ItemLocationType::Path)` + pub fn location_from(&self) -> &[ItemLocation] { + &self.location_from + } + + /// Returns where the interaction goes to. + /// + /// Example: + /// + /// 1. `ItemLocation::new("app.domain.com", ItemLocationType::Host)` + /// 2. `ItemLocation::new("http://app.domain.com/resource", + /// ItemLocationType::Path)` + pub fn location_to(&self) -> &[ItemLocation] { + &self.location_to + } +} diff --git a/crate/item_model/src/item_interaction/item_interaction_within.rs b/crate/item_model/src/item_interaction/item_interaction_within.rs new file mode 100644 index 000000000..2f264af11 --- /dev/null +++ b/crate/item_model/src/item_interaction/item_interaction_within.rs @@ -0,0 +1,29 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ItemLocation, ItemLocationAncestors}; + +/// Represents a resource interaction that happens within a location. +/// +/// This can represent application installation / startup happening on a +/// server. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct ItemInteractionWithin { + /// Where the interaction is happening. + /// + /// Example: + /// + /// 1. `ItemLocation::Server { address, port: None }` + pub location: ItemLocationAncestors, +} + +impl ItemInteractionWithin { + /// Returns a new `ItemInteractionWithin`. + pub fn new(location: ItemLocationAncestors) -> Self { + Self { location } + } + + /// Returns where the interaction is happening. + pub fn location(&self) -> &[ItemLocation] { + &self.location + } +} diff --git a/crate/item_model/src/item_interactions_current.rs b/crate/item_model/src/item_interactions_current.rs new file mode 100644 index 000000000..79075b008 --- /dev/null +++ b/crate/item_model/src/item_interactions_current.rs @@ -0,0 +1,54 @@ +use std::ops::{Deref, DerefMut}; + +use serde::{Deserialize, Serialize}; + +use crate::ItemInteraction; + +/// [`ItemInteraction`]s constructed from parameters derived from fully known +/// state. +#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] +pub struct ItemInteractionsCurrent(Vec); + +impl ItemInteractionsCurrent { + /// Returns a new `ItemInteractionsCurrent` map. + pub fn new() -> Self { + Self::default() + } + + /// Returns a new `ItemInteractionsCurrent` map with the given preallocated + /// capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self(Vec::with_capacity(capacity)) + } + + /// Returns the underlying map. + pub fn into_inner(self) -> Vec { + self.0 + } +} + +impl Deref for ItemInteractionsCurrent { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for ItemInteractionsCurrent { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl From> for ItemInteractionsCurrent { + fn from(inner: Vec) -> Self { + Self(inner) + } +} + +impl FromIterator for ItemInteractionsCurrent { + fn from_iter>(iter: I) -> Self { + Self(Vec::from_iter(iter)) + } +} diff --git a/crate/item_model/src/item_interactions_current_or_example.rs b/crate/item_model/src/item_interactions_current_or_example.rs new file mode 100644 index 000000000..9b3330612 --- /dev/null +++ b/crate/item_model/src/item_interactions_current_or_example.rs @@ -0,0 +1,27 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ItemInteractionsCurrent, ItemInteractionsExample}; + +/// [`ItemInteraction`]s constructed from parameters derived from at least some +/// example state. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub enum ItemInteractionsCurrentOrExample { + /// [`ItemInteraction`]s constructed from parameters derived from fully + /// known state. + Current(ItemInteractionsCurrent), + /// [`ItemInteraction`]s constructed from parameters derived from at least + /// some example state. + Example(ItemInteractionsExample), +} + +impl From for ItemInteractionsCurrentOrExample { + fn from(item_interactions_current: ItemInteractionsCurrent) -> Self { + Self::Current(item_interactions_current) + } +} + +impl From for ItemInteractionsCurrentOrExample { + fn from(item_interactions_example: ItemInteractionsExample) -> Self { + Self::Example(item_interactions_example) + } +} diff --git a/crate/item_model/src/item_interactions_example.rs b/crate/item_model/src/item_interactions_example.rs new file mode 100644 index 000000000..acf9928f9 --- /dev/null +++ b/crate/item_model/src/item_interactions_example.rs @@ -0,0 +1,54 @@ +use std::ops::{Deref, DerefMut}; + +use serde::{Deserialize, Serialize}; + +use crate::ItemInteraction; + +/// [`ItemInteraction`]s constructed from parameters derived from at least some +/// example state. +#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] +pub struct ItemInteractionsExample(Vec); + +impl ItemInteractionsExample { + /// Returns a new `ItemInteractionsExample` map. + pub fn new() -> Self { + Self::default() + } + + /// Returns a new `ItemInteractionsExample` map with the given preallocated + /// capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self(Vec::with_capacity(capacity)) + } + + /// Returns the underlying map. + pub fn into_inner(self) -> Vec { + self.0 + } +} + +impl Deref for ItemInteractionsExample { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for ItemInteractionsExample { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl From> for ItemInteractionsExample { + fn from(inner: Vec) -> Self { + Self(inner) + } +} + +impl FromIterator for ItemInteractionsExample { + fn from_iter>(iter: I) -> Self { + Self(Vec::from_iter(iter)) + } +} diff --git a/crate/item_model/src/item_location.rs b/crate/item_model/src/item_location.rs new file mode 100644 index 000000000..474eae11c --- /dev/null +++ b/crate/item_model/src/item_location.rs @@ -0,0 +1,204 @@ +use std::path::Path; + +use serde::{Deserialize, Serialize}; +use url::Url; + +use crate::ItemLocationType; + +/// One layer of where a resource is located. +/// +/// These will be merged into the same node based on their variant and name. +/// +/// For example, if two different items provide the following +/// `ItemLocation`s: +/// +/// Item 1: +/// +/// 1. `ItemLocation::Group("cloud")` +/// 2. `ItemLocation::Host("app.domain.com")` +/// 3. `ItemLocation::Path("/path/to/a_file")` +/// +/// Item 2: +/// +/// 1. `ItemLocation::Host("app.domain.com")` +/// 2. `ItemLocation::Path("/path/to/another_file")` +/// +/// Then the resultant node hierarchy will be: +/// +/// ```yaml +/// cloud: +/// app.domain.com: +/// "/path/to/a_file": {} +/// "/path/to/another_file": {} +/// ``` +/// +/// # Implementors +/// +/// Item implementors should endeavour to use the same name for each +/// `ItemLocation`, as that is how the Peace framework determines if two +/// `ItemLocation`s are the same. +/// +/// # Design +/// +/// When designing this, another design that was considered is using an enum +/// like the following: +/// +/// ```rust,ignore +/// #[derive(Debug)] +/// enum ItemLocation { +/// Host(ItemLocationHost), +/// Url(Url), +/// } +/// +/// struct ItemLocationHost { +/// host: Host, +/// port: Option, +/// } +/// +/// impl ItemLocation { +/// fn from_url(url: &Url) -> Self { +/// Self::Url(url.clone()) +/// } +/// } +/// +/// impl From<&Url> for ItemLocationHost { +/// type Error = (); +/// +/// fn from(url: &Url) -> Result { +/// url.host() +/// .map(|host| { +/// let host = host.to_owned(); +/// let port = url.map(Url::port_or_known_default); +/// Self { host, port } +/// }) +/// .ok_or(()) +/// } +/// } +/// ``` +/// +/// However, the purpose of `ItemLocation` is primarily for rendering, and +/// providing accurate variants for each kind of resource location causes +/// additional burden on: +/// +/// * framework maintainers to maintain those variants +/// * item implementors to select the correct variant for accuracy +/// * item implementors to select a variant consistent with other item +/// implementors +/// +/// A less accurate model with a limited number of [`ItemLocationType`]s +/// balances the modelling accuracy, rendering, and maintenance burden. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)] +pub struct ItemLocation { + /// The type of the resource location. + pub r#type: ItemLocationType, + /// The name of the resource location. + pub name: String, +} + +impl ItemLocation { + /// The string used for an unknown host. + pub const HOST_UNKNOWN: &'static str = "unknown"; + /// The string used for localhost. + pub const LOCALHOST: &'static str = "💻 localhost"; + + /// Returns a new `ItemLocation`. + /// + /// See also: + /// + /// * [`ItemLocation::group`] + /// * [`ItemLocation::host`] + /// * [`ItemLocation::localhost`] + /// * [`ItemLocation::path`] + pub fn new(r#type: ItemLocationType, name: String) -> Self { + Self { r#type, name } + } + + /// Returns `ItemLocation::new(name, ItemLocationType::Group)`. + pub fn group(name: String) -> Self { + Self { + r#type: ItemLocationType::Group, + name, + } + } + + /// Returns `ItemLocation::new(name, ItemLocationType::Host)`. + pub fn host(name: String) -> Self { + Self { + r#type: ItemLocationType::Host, + name, + } + } + + /// Returns `ItemLocation::new("unknown".to_string(), + /// ItemLocationType::Host)`. + pub fn host_unknown() -> Self { + Self { + r#type: ItemLocationType::Host, + name: Self::HOST_UNKNOWN.to_string(), + } + } + + /// Returns `ItemLocation::new(name, ItemLocationType::Host)`. + /// + /// This is "lossy" in the sense that if the URL doesn't have a [`Host`], + /// this will return localhost, as URLs without a host may be unix sockets, + /// or data URLs. + /// + /// [`Host`]: url::Host + pub fn host_from_url(url: &Url) -> Self { + url.host_str() + .map(|host_str| Self { + r#type: ItemLocationType::Host, + name: format!("🌐 {host_str}"), + }) + .unwrap_or_else(Self::localhost) + } + + /// Returns `ItemLocation::host("localhost".to_string())`. + pub fn localhost() -> Self { + Self { + r#type: ItemLocationType::Host, + name: Self::LOCALHOST.to_string(), + } + } + + /// Returns `ItemLocation::new(name, ItemLocationType::Path)`. + /// + /// See also [`ItemLocation::path_lossy`]. + /// + /// [`ItemLocation::path_lossy`]: Self::path_lossy + pub fn path(name: String) -> Self { + Self { + r#type: ItemLocationType::Path, + name, + } + } + + /// Returns `ItemLocation::new(name, ItemLocationType::Path)`, using the + /// lossy conversion from the given path. + pub fn path_lossy(name: &Path) -> Self { + Self { + // For some reason, calling `to_string_lossy()` on the path itself doesn't return the + // replacement character, and breaks the `item_model::item_location::path_lossy` test. + // + // The rust source code on 1.80.0 stable uses `String::from_utf8_lossy` internally: + // + // + // ```rust + // name.to_string_lossy().to_string() + // ``` + name: String::from_utf8_lossy(name.as_os_str().as_encoded_bytes()).to_string(), + r#type: ItemLocationType::Path, + } + } + + /// Returns the name of the resource location. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the type of the resource location. + pub fn r#type(&self) -> ItemLocationType { + self.r#type + } +} diff --git a/crate/item_model/src/item_location_ancestors.rs b/crate/item_model/src/item_location_ancestors.rs new file mode 100644 index 000000000..17e6f39b8 --- /dev/null +++ b/crate/item_model/src/item_location_ancestors.rs @@ -0,0 +1,51 @@ +use std::ops::{Deref, DerefMut}; + +use serde::{Deserialize, Serialize}; + +use crate::ItemLocation; + +/// An [`ItemLocation`] and its ancestors. +/// +/// The list of [`ItemLocation`]s within this container starts with the +/// outermost known ancestor, gradually moving closer to the innermost +/// `ItemLocation`. +#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] +pub struct ItemLocationAncestors(Vec); + +impl ItemLocationAncestors { + /// Returns a new [`ItemLocationAncestors`] with the given ancestry. + pub fn new(ancestors: Vec) -> Self { + Self(ancestors) + } + + /// Returns the underlying `Vec`. + pub fn into_inner(self) -> Vec { + self.0 + } +} + +impl Deref for ItemLocationAncestors { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for ItemLocationAncestors { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl From for ItemLocationAncestors { + fn from(item_location: ItemLocation) -> Self { + Self(vec![item_location]) + } +} + +impl From> for ItemLocationAncestors { + fn from(ancestors: Vec) -> Self { + Self(ancestors) + } +} diff --git a/crate/item_model/src/item_location_state_in_progress.rs b/crate/item_model/src/item_location_state_in_progress.rs new file mode 100644 index 000000000..8c51db799 --- /dev/null +++ b/crate/item_model/src/item_location_state_in_progress.rs @@ -0,0 +1,122 @@ +use peace_core::progress::{CmdBlockItemInteractionType, ProgressComplete, ProgressStatus}; +use serde::{Deserialize, Serialize}; + +use crate::ItemLocationState; + +/// Represents the state of an [`ItemLocation`]. +/// +/// This affects how the [`ItemLocation`] is rendered. +/// +/// This is analogous to [`ItemLocationState`], with added variants for when the +/// state is being determined. +/// +/// [`ItemLocation`]: crate::ItemLocation +#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub enum ItemLocationStateInProgress { + /// [`ItemLocation`] does not exist. + /// + /// This means it should be rendered invisible / low opacity. + /// + /// [`ItemLocation`]: crate::ItemLocation + NotExists, + /// [`ItemLocation`] should not exist, but does. + /// + /// This means it should be rendered red outlined with full opacity. + /// + /// [`ItemLocation`]: crate::ItemLocation + NotExistsError, + /// [`ItemLocation`] may or may not exist, and we are in the process of + /// determining that. + /// + /// This means it should be rendered pulsing / mid opacity. + /// + /// [`ItemLocation`]: crate::ItemLocation + DiscoverInProgress, + /// [`ItemLocation`] may or may not exist, and we failed to discover that. + /// + /// This means it should be rendered red / mid opacity. + /// + /// [`ItemLocation`]: crate::ItemLocation + DiscoverError, + /// [`ItemLocation`] is being created. + /// + /// This means it should be rendered with full opacity and blue animated + /// outlines. + /// + /// [`ItemLocation`]: crate::ItemLocation + CreateInProgress, + /// [`ItemLocation`] is being modified. + /// + /// This means it should be rendered with full opacity and blue animated + /// outlines. + /// + /// [`ItemLocation`]: crate::ItemLocation + ModificationInProgress, + /// [`ItemLocation`] exists. + /// + /// This means it should be rendered with full opacity. + /// + /// [`ItemLocation`]: crate::ItemLocation + ExistsOk, + /// [`ItemLocation`] exists, but is in an erroneous state. + /// + /// This means it should be rendered with full opacity with a red shape + /// colour. + /// + /// [`ItemLocation`]: crate::ItemLocation + ExistsError, +} + +impl ItemLocationStateInProgress { + #[rustfmt::skip] + pub fn from( + cmd_block_item_interaction_type: CmdBlockItemInteractionType, + item_location_state: ItemLocationState, + progress_status: ProgressStatus, + ) -> Self { + match ( + cmd_block_item_interaction_type, + item_location_state, + progress_status, + ) { + (CmdBlockItemInteractionType::Write, ItemLocationState::NotExists, ProgressStatus::Initialized) => Self::NotExists, + (CmdBlockItemInteractionType::Write, ItemLocationState::NotExists, ProgressStatus::Interrupted) => Self::NotExists, + (CmdBlockItemInteractionType::Write, ItemLocationState::NotExists, ProgressStatus::ExecPending) => Self::NotExists, + (CmdBlockItemInteractionType::Write, ItemLocationState::NotExists, ProgressStatus::Queued) => Self::NotExists, + (CmdBlockItemInteractionType::Write, ItemLocationState::NotExists, ProgressStatus::Running) => Self::CreateInProgress, + (CmdBlockItemInteractionType::Write, ItemLocationState::NotExists, ProgressStatus::RunningStalled) => Self::CreateInProgress, + (CmdBlockItemInteractionType::Write, ItemLocationState::NotExists, ProgressStatus::UserPending) => Self::CreateInProgress, + (CmdBlockItemInteractionType::Write, ItemLocationState::NotExists, ProgressStatus::Complete(ProgressComplete::Success)) => Self::NotExists, + (CmdBlockItemInteractionType::Write, ItemLocationState::NotExists, ProgressStatus::Complete(ProgressComplete::Fail)) => Self::NotExistsError, + (CmdBlockItemInteractionType::Write, ItemLocationState::Exists, ProgressStatus::Initialized) => Self::ExistsOk, + (CmdBlockItemInteractionType::Write, ItemLocationState::Exists, ProgressStatus::Interrupted) => Self::ExistsOk, + (CmdBlockItemInteractionType::Write, ItemLocationState::Exists, ProgressStatus::ExecPending) => Self::ExistsOk, + (CmdBlockItemInteractionType::Write, ItemLocationState::Exists, ProgressStatus::Queued) => Self::ExistsOk, + (CmdBlockItemInteractionType::Write, ItemLocationState::Exists, ProgressStatus::Running) => Self::ModificationInProgress, + (CmdBlockItemInteractionType::Write, ItemLocationState::Exists, ProgressStatus::RunningStalled) => Self::ModificationInProgress, + (CmdBlockItemInteractionType::Write, ItemLocationState::Exists, ProgressStatus::UserPending) => Self::ModificationInProgress, + (CmdBlockItemInteractionType::Write, ItemLocationState::Exists, ProgressStatus::Complete(ProgressComplete::Success)) => Self::ExistsOk, + (CmdBlockItemInteractionType::Write, ItemLocationState::Exists, ProgressStatus::Complete(ProgressComplete::Fail)) => Self::ExistsError, + (CmdBlockItemInteractionType::Read, ItemLocationState::NotExists, ProgressStatus::Initialized) => Self::NotExists, + (CmdBlockItemInteractionType::Read, ItemLocationState::NotExists, ProgressStatus::Interrupted) => Self::NotExists, + (CmdBlockItemInteractionType::Read, ItemLocationState::NotExists, ProgressStatus::ExecPending) => Self::NotExists, + (CmdBlockItemInteractionType::Read, ItemLocationState::NotExists, ProgressStatus::Queued) => Self::NotExists, + (CmdBlockItemInteractionType::Read, ItemLocationState::NotExists, ProgressStatus::Running) => Self::DiscoverInProgress, + (CmdBlockItemInteractionType::Read, ItemLocationState::NotExists, ProgressStatus::RunningStalled) => Self::DiscoverInProgress, + (CmdBlockItemInteractionType::Read, ItemLocationState::NotExists, ProgressStatus::UserPending) => Self::DiscoverInProgress, + (CmdBlockItemInteractionType::Read, ItemLocationState::NotExists, ProgressStatus::Complete(ProgressComplete::Success)) => Self::NotExists, + (CmdBlockItemInteractionType::Read, ItemLocationState::NotExists, ProgressStatus::Complete(ProgressComplete::Fail)) => Self::DiscoverError, + (CmdBlockItemInteractionType::Read, ItemLocationState::Exists, ProgressStatus::Initialized) => Self::ExistsOk, + (CmdBlockItemInteractionType::Read, ItemLocationState::Exists, ProgressStatus::Interrupted) => Self::ExistsOk, + (CmdBlockItemInteractionType::Read, ItemLocationState::Exists, ProgressStatus::ExecPending) => Self::ExistsOk, + (CmdBlockItemInteractionType::Read, ItemLocationState::Exists, ProgressStatus::Queued) => Self::ExistsOk, + (CmdBlockItemInteractionType::Read, ItemLocationState::Exists, ProgressStatus::Running) => Self::ModificationInProgress, + (CmdBlockItemInteractionType::Read, ItemLocationState::Exists, ProgressStatus::RunningStalled) => Self::ModificationInProgress, + (CmdBlockItemInteractionType::Read, ItemLocationState::Exists, ProgressStatus::UserPending) => Self::ModificationInProgress, + (CmdBlockItemInteractionType::Read, ItemLocationState::Exists, ProgressStatus::Complete(ProgressComplete::Success)) => Self::ExistsOk, + (CmdBlockItemInteractionType::Read, ItemLocationState::Exists, ProgressStatus::Complete(ProgressComplete::Fail)) => Self::ExistsError, + (CmdBlockItemInteractionType::Local, ItemLocationState::NotExists, _) => Self::NotExists, + (CmdBlockItemInteractionType::Local, ItemLocationState::Exists, _) => Self::ExistsOk, + } + } +} diff --git a/crate/item_model/src/item_location_tree.rs b/crate/item_model/src/item_location_tree.rs new file mode 100644 index 000000000..8afe99115 --- /dev/null +++ b/crate/item_model/src/item_location_tree.rs @@ -0,0 +1,42 @@ +use serde::{Deserialize, Serialize}; + +use crate::ItemLocation; + +/// An [`ItemLocation`] and its children. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct ItemLocationTree { + /// This [`ItemLocation`]. + pub item_location: ItemLocation, + /// The children of this [`ItemLocation`]. + pub children: Vec, +} + +impl ItemLocationTree { + /// Returns a new [`ItemLocationTree`]. + pub fn new(item_location: ItemLocation, children: Vec) -> Self { + Self { + item_location, + children, + } + } + + /// Returns this [`ItemLocation`]. + pub fn item_location(&self) -> &ItemLocation { + &self.item_location + } + + /// Returns the children of this [`ItemLocation`]. + pub fn children(&self) -> &[ItemLocationTree] { + &self.children + } + + /// Returns the total number of [`ItemLocation`]s within this tree, + /// including itself. + pub fn item_location_count(&self) -> usize { + 1 + self + .children + .iter() + .map(ItemLocationTree::item_location_count) + .sum::() + } +} diff --git a/crate/item_model/src/item_location_type.rs b/crate/item_model/src/item_location_type.rs new file mode 100644 index 000000000..ae967fe23 --- /dev/null +++ b/crate/item_model/src/item_location_type.rs @@ -0,0 +1,31 @@ +use serde::{Deserialize, Serialize}; + +/// The type of resource location. +/// +/// This affects how the [`ItemLocation`] is rendered. +/// +/// [`ItemLocation`]: crate::ItemLocation +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Ord, Eq, Hash, Deserialize, Serialize)] +pub enum ItemLocationType { + /// Rendered with dashed lines. + /// + /// Suitable for concepts like: + /// + /// * Cloud provider + /// * Network / subnet + Group, + /// Rendered with solid lines. + /// + /// Suitable for concepts like: + /// + /// * Localhost + /// * Server + Host, + /// Rendered with solid lines. + /// + /// Suitable for concepts like: + /// + /// * File path + /// * URL + Path, +} diff --git a/crate/item_model/src/item_locations_and_interactions.rs b/crate/item_model/src/item_locations_and_interactions.rs new file mode 100644 index 000000000..520a82db2 --- /dev/null +++ b/crate/item_model/src/item_locations_and_interactions.rs @@ -0,0 +1,79 @@ +use indexmap::IndexMap; +use peace_core::ItemId; +use serde::{Deserialize, Serialize}; + +use crate::{ItemInteraction, ItemLocationTree}; + +#[cfg(feature = "output_progress")] +use std::collections::{HashMap, HashSet}; + +#[cfg(feature = "output_progress")] +use crate::ItemLocation; + +/// Merged [`ItemLocation`]s and [`ItemInteraction`]s from all items. +/// +/// [`ItemLocation`]: crate::ItemLocation +#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] +pub struct ItemLocationsAndInteractions { + /// Hierarchical storage of [`ItemLocation`]s. + /// + /// [`ItemLocation`]: crate::ItemLocation + pub item_location_trees: Vec, + /// The [`ItemInteraction`]s from each item. + pub item_to_item_interactions: IndexMap>, + /// Number of `ItemLocation`s from all merged [`ItemInteraction`]s. + /// + /// [`ItemLocation`]: crate::ItemLocation + pub item_location_count: usize, + /// Map that tracks the items that referred to each item location. + #[cfg(feature = "output_progress")] + pub item_location_to_item_id_sets: HashMap>, +} + +impl ItemLocationsAndInteractions { + /// Returns a new `ItemLocationsAndInteractions` container. + pub fn new( + item_location_trees: Vec, + item_to_item_interactions: IndexMap>, + item_location_count: usize, + #[cfg(feature = "output_progress")] item_location_to_item_id_sets: HashMap< + ItemLocation, + HashSet, + >, + ) -> Self { + Self { + item_location_trees, + item_to_item_interactions, + item_location_count, + #[cfg(feature = "output_progress")] + item_location_to_item_id_sets, + } + } + + /// Returns the hierarchical storage of [`ItemLocation`]s. + /// + /// [`ItemLocation`]: crate::ItemLocation + pub fn item_location_trees(&self) -> &[ItemLocationTree] { + &self.item_location_trees + } + + /// Returns the [`ItemInteraction`]s from each item. + pub fn item_to_item_interactions(&self) -> &IndexMap> { + &self.item_to_item_interactions + } + + /// Returns the number of `ItemLocation`s from all merged + /// [`ItemInteraction`]s. + /// + /// [`ItemLocation`]: crate::ItemLocation + pub fn item_location_count(&self) -> usize { + self.item_location_count + } + + /// Returns the map that tracks the items that referred to each item + /// location. + #[cfg(feature = "output_progress")] + pub fn item_location_to_item_id_sets(&self) -> &HashMap> { + &self.item_location_to_item_id_sets + } +} diff --git a/crate/item_model/src/item_locations_combined.rs b/crate/item_model/src/item_locations_combined.rs new file mode 100644 index 000000000..032fc24d2 --- /dev/null +++ b/crate/item_model/src/item_locations_combined.rs @@ -0,0 +1,53 @@ +use std::ops::{Deref, DerefMut}; + +use serde::{Deserialize, Serialize}; + +use crate::ItemLocationTree; + +/// All [`ItemLocation`]s from all items merged and deduplicated. +#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] +pub struct ItemLocationsCombined(Vec); + +impl ItemLocationsCombined { + /// Returns a new `ItemLocationsCombined`. + pub fn new() -> Self { + Self::default() + } + + /// Returns a new `ItemLocationsCombined` map with the given preallocated + /// capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self(Vec::with_capacity(capacity)) + } + + /// Returns the underlying map. + pub fn into_inner(self) -> Vec { + self.0 + } +} + +impl Deref for ItemLocationsCombined { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for ItemLocationsCombined { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl From> for ItemLocationsCombined { + fn from(inner: Vec) -> Self { + Self(inner) + } +} + +impl FromIterator for ItemLocationsCombined { + fn from_iter>(iter: I) -> Self { + Self(Vec::from_iter(iter)) + } +} diff --git a/crate/item_model/src/lib.rs b/crate/item_model/src/lib.rs new file mode 100644 index 000000000..073c992e1 --- /dev/null +++ b/crate/item_model/src/lib.rs @@ -0,0 +1,51 @@ +//! Data types for resource interactions for the Peace framework. + +// Re-exports +pub use url::{self, Host, Url}; + +// TODO: Remove this when we have refactored where progress types live. +#[cfg(feature = "output_progress")] +pub use peace_core::progress::ItemLocationState; + +pub use crate::{ + item_interaction::{ + ItemInteraction, ItemInteractionPull, ItemInteractionPush, ItemInteractionWithin, + }, + item_interactions_current::ItemInteractionsCurrent, + item_interactions_current_or_example::ItemInteractionsCurrentOrExample, + item_interactions_example::ItemInteractionsExample, + item_location::ItemLocation, + item_location_ancestors::ItemLocationAncestors, + item_location_tree::ItemLocationTree, + item_location_type::ItemLocationType, + item_locations_combined::ItemLocationsCombined, +}; + +#[cfg(feature = "item_locations_and_interactions")] +pub use crate::item_locations_and_interactions::ItemLocationsAndInteractions; + +#[cfg(feature = "output_progress")] +pub use crate::{ + // TODO: uncomment when we have refactored where progress types live. + // item_location_state::ItemLocationState, + item_location_state_in_progress::ItemLocationStateInProgress, +}; + +mod item_interaction; +mod item_interactions_current; +mod item_interactions_current_or_example; +mod item_interactions_example; +mod item_location; +mod item_location_ancestors; +mod item_location_tree; +mod item_location_type; +mod item_locations_combined; + +// TODO: uncomment when we have refactored where progress types live. +// #[cfg(feature = "output_progress")] +// mod item_location_state; +#[cfg(feature = "output_progress")] +mod item_location_state_in_progress; + +#[cfg(feature = "item_locations_and_interactions")] +mod item_locations_and_interactions; diff --git a/crate/params/Cargo.toml b/crate/params/Cargo.toml index cfd1bf91d..c9a023416 100644 --- a/crate/params/Cargo.toml +++ b/crate/params/Cargo.toml @@ -35,3 +35,4 @@ tynm = { workspace = true } [features] default = [] error_reporting = ["dep:miette"] +item_state_example = ["peace_data/item_state_example"] diff --git a/crate/params/src/any_spec_data_type.rs b/crate/params/src/any_spec_data_type.rs index f30b50899..62f3e2b5a 100644 --- a/crate/params/src/any_spec_data_type.rs +++ b/crate/params/src/any_spec_data_type.rs @@ -23,7 +23,7 @@ impl Clone for Box { } } -impl<'a> serde::Serialize for dyn AnySpecDataType + 'a { +impl serde::Serialize for dyn AnySpecDataType + '_ { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, diff --git a/crate/params/src/lib.rs b/crate/params/src/lib.rs index 6b5873ee0..f3bc7cd69 100644 --- a/crate/params/src/lib.rs +++ b/crate/params/src/lib.rs @@ -76,6 +76,7 @@ pub use crate::{ mapping_fn_impl::MappingFnImpl, params::Params, params_fieldless::ParamsFieldless, + params_merge_ext::ParamsMergeExt, params_resolve_error::ParamsResolveError, params_spec::ParamsSpec, params_spec_de::ParamsSpecDe, @@ -99,6 +100,7 @@ mod mapping_fn; mod mapping_fn_impl; mod params; mod params_fieldless; +mod params_merge_ext; mod params_resolve_error; mod params_spec; mod params_spec_de; diff --git a/crate/params/src/mapping_fn.rs b/crate/params/src/mapping_fn.rs index 2abae1f55..c45c13cd1 100644 --- a/crate/params/src/mapping_fn.rs +++ b/crate/params/src/mapping_fn.rs @@ -56,7 +56,7 @@ impl Clone for Box> { } } -impl<'a, T> Serialize for dyn MappingFn + 'a { +impl Serialize for dyn MappingFn + '_ { fn serialize(&self, serializer: S) -> Result where S: Serializer, diff --git a/crate/params/src/mapping_fn_impl.rs b/crate/params/src/mapping_fn_impl.rs index b080cdc2a..64b183cd5 100644 --- a/crate/params/src/mapping_fn_impl.rs +++ b/crate/params/src/mapping_fn_impl.rs @@ -11,6 +11,9 @@ use crate::{ FromFunc, Func, MappingFn, ParamsResolveError, ValueResolutionCtx, ValueResolutionMode, }; +#[cfg(feature = "item_state_example")] +use peace_data::marker::Example; + /// Wrapper around a mapping function so that it can be serialized. #[derive(Clone, Serialize, Deserialize)] pub struct MappingFnImpl { @@ -141,8 +144,17 @@ macro_rules! impl_mapping_fn_impl { // We have to duplicate code because the return type from // `resources.try_borrow` is different per branch. match value_resolution_ctx.value_resolution_mode() { - ValueResolutionMode::ApplyDry => { - $(arg_resolve!(resources, value_resolution_ctx, ApplyDry, $var, $Arg);)+ + #[cfg(feature = "item_state_example")] + ValueResolutionMode::Example => { + $(arg_resolve!(resources, value_resolution_ctx, Example, $var, $Arg);)+ + + fn_map($(&$var,)+).ok_or(ParamsResolveError::FromMap { + value_resolution_ctx: value_resolution_ctx.clone(), + from_type_name: tynm::type_name::<($($Arg,)+)>(), + }) + } + ValueResolutionMode::Clean => { + $(arg_resolve!(resources, value_resolution_ctx, Clean, $var, $Arg);)+ fn_map($(&$var,)+).ok_or(ParamsResolveError::FromMap { value_resolution_ctx: value_resolution_ctx.clone(), @@ -165,8 +177,8 @@ macro_rules! impl_mapping_fn_impl { from_type_name: tynm::type_name::<($($Arg,)+)>(), }) } - ValueResolutionMode::Clean => { - $(arg_resolve!(resources, value_resolution_ctx, Clean, $var, $Arg);)+ + ValueResolutionMode::ApplyDry => { + $(arg_resolve!(resources, value_resolution_ctx, ApplyDry, $var, $Arg);)+ fn_map($(&$var,)+).ok_or(ParamsResolveError::FromMap { value_resolution_ctx: value_resolution_ctx.clone(), @@ -205,8 +217,14 @@ macro_rules! impl_mapping_fn_impl { // We have to duplicate code because the return type from // `resources.try_borrow` is different per branch. match value_resolution_ctx.value_resolution_mode() { - ValueResolutionMode::ApplyDry => { - $(try_arg_resolve!(resources, value_resolution_ctx, ApplyDry, $var, $Arg);)+ + #[cfg(feature = "item_state_example")] + ValueResolutionMode::Example => { + $(try_arg_resolve!(resources, value_resolution_ctx, Example, $var, $Arg);)+ + + Ok(fn_map($(&$var,)+)) + } + ValueResolutionMode::Clean => { + $(try_arg_resolve!(resources, value_resolution_ctx, Clean, $var, $Arg);)+ Ok(fn_map($(&$var,)+)) } @@ -220,8 +238,8 @@ macro_rules! impl_mapping_fn_impl { Ok(fn_map($(&$var,)+)) } - ValueResolutionMode::Clean => { - $(try_arg_resolve!(resources, value_resolution_ctx, Clean, $var, $Arg);)+ + ValueResolutionMode::ApplyDry => { + $(try_arg_resolve!(resources, value_resolution_ctx, ApplyDry, $var, $Arg);)+ Ok(fn_map($(&$var,)+)) } diff --git a/crate/params/src/params_merge_ext.rs b/crate/params/src/params_merge_ext.rs new file mode 100644 index 000000000..bad5d4811 --- /dev/null +++ b/crate/params/src/params_merge_ext.rs @@ -0,0 +1,11 @@ +use crate::Params; + +/// Trait for merging `ParamsPartial` onto a `Params` object. +/// +/// This is automatically implemented by [`#[derive(Params)]`]. +/// +/// [`#[derive(Params)]`]: peace_params_derive::Params +pub trait ParamsMergeExt: Params { + /// Moves the values from `Self::Partial` onto this `Params` object. + fn merge(&mut self, params_partial: ::Partial); +} diff --git a/crate/params/src/params_spec.rs b/crate/params/src/params_spec.rs index 7f5db83dc..8c2dbd27f 100644 --- a/crate/params/src/params_spec.rs +++ b/crate/params/src/params_spec.rs @@ -5,7 +5,7 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize}; use crate::{ AnySpecDataType, AnySpecRt, FieldWiseSpecRt, MappingFn, MappingFnImpl, Params, - ParamsResolveError, ValueResolutionCtx, ValueSpecRt, + ParamsResolveError, ValueResolutionCtx, ValueResolutionMode, ValueSpecRt, }; /// How to populate a field's value in an item's params. @@ -144,19 +144,45 @@ where ) -> Result { match self { ParamsSpec::Value { value } => Ok(value.clone()), - ParamsSpec::Stored | ParamsSpec::InMemory => match resources.try_borrow::() { - Ok(value) => Ok((*value).clone()), - Err(borrow_fail) => match borrow_fail { - BorrowFail::ValueNotFound => Err(ParamsResolveError::InMemory { - value_resolution_ctx: value_resolution_ctx.clone(), - }), - BorrowFail::BorrowConflictImm | BorrowFail::BorrowConflictMut => { - Err(ParamsResolveError::InMemoryBorrowConflict { - value_resolution_ctx: value_resolution_ctx.clone(), - }) + ParamsSpec::Stored | ParamsSpec::InMemory => { + // Try resolve `T`, through the `value_resolution_ctx` first + let params_resolved = match value_resolution_ctx.value_resolution_mode() { + #[cfg(feature = "item_state_example")] + ValueResolutionMode::Example => resources + .try_borrow::>() + .map(|data_marker| data_marker.0.clone()), + ValueResolutionMode::Clean => resources + .try_borrow::>() + .map(|data_marker| data_marker.0.clone()), + ValueResolutionMode::Current => resources + .try_borrow::>() + .map(|data_marker| data_marker.0.clone()), + ValueResolutionMode::Goal => resources + .try_borrow::>() + .map(|data_marker| data_marker.0.clone()), + ValueResolutionMode::ApplyDry => resources + .try_borrow::>() + .map(|data_marker| data_marker.0.clone()), + } + .and_then(|param_opt| param_opt.ok_or(BorrowFail::ValueNotFound)); + + params_resolved.or_else(|_e| { + // Try resolve `T` again without the `value_resolution_ctx` wrapper. + match resources.try_borrow::() { + Ok(value) => Ok((*value).clone()), + Err(borrow_fail) => match borrow_fail { + BorrowFail::ValueNotFound => Err(ParamsResolveError::InMemory { + value_resolution_ctx: value_resolution_ctx.clone(), + }), + BorrowFail::BorrowConflictImm | BorrowFail::BorrowConflictMut => { + Err(ParamsResolveError::InMemoryBorrowConflict { + value_resolution_ctx: value_resolution_ctx.clone(), + }) + } + }, } - }, - }, + }) + } ParamsSpec::MappingFn(mapping_fn) => mapping_fn.map(resources, value_resolution_ctx), ParamsSpec::FieldWise { field_wise_spec } => { field_wise_spec.resolve(resources, value_resolution_ctx) @@ -171,17 +197,43 @@ where ) -> Result { match self { ParamsSpec::Value { value } => Ok(T::Partial::from((*value).clone())), - ParamsSpec::Stored | ParamsSpec::InMemory => match resources.try_borrow::() { - Ok(value) => Ok(T::Partial::from((*value).clone())), - Err(borrow_fail) => match borrow_fail { - BorrowFail::ValueNotFound => Ok(T::Partial::default()), - BorrowFail::BorrowConflictImm | BorrowFail::BorrowConflictMut => { - Err(ParamsResolveError::InMemoryBorrowConflict { - value_resolution_ctx: value_resolution_ctx.clone(), - }) + ParamsSpec::Stored | ParamsSpec::InMemory => { + // Try resolve `T`, through the `value_resolution_ctx` first + let params_partial_resolved = match value_resolution_ctx.value_resolution_mode() { + #[cfg(feature = "item_state_example")] + ValueResolutionMode::Example => resources + .try_borrow::>() + .map(|data_marker| data_marker.0.clone()), + ValueResolutionMode::Clean => resources + .try_borrow::>() + .map(|data_marker| data_marker.0.clone()), + ValueResolutionMode::Current => resources + .try_borrow::>() + .map(|data_marker| data_marker.0.clone()), + ValueResolutionMode::Goal => resources + .try_borrow::>() + .map(|data_marker| data_marker.0.clone()), + ValueResolutionMode::ApplyDry => resources + .try_borrow::>() + .map(|data_marker| data_marker.0.clone()), + } + .and_then(|param_opt| param_opt.ok_or(BorrowFail::ValueNotFound)); + + params_partial_resolved.map(T::Partial::from).or_else(|_e| { + // Try resolve `T` again without the `value_resolution_ctx` wrapper. + match resources.try_borrow::() { + Ok(value) => Ok(T::Partial::from((*value).clone())), + Err(borrow_fail) => match borrow_fail { + BorrowFail::ValueNotFound => Ok(T::Partial::default()), + BorrowFail::BorrowConflictImm | BorrowFail::BorrowConflictMut => { + Err(ParamsResolveError::InMemoryBorrowConflict { + value_resolution_ctx: value_resolution_ctx.clone(), + }) + } + }, } - }, - }, + }) + } ParamsSpec::MappingFn(mapping_fn) => mapping_fn .try_map(resources, value_resolution_ctx) .map(|t| t.map(T::Partial::from).unwrap_or_default()), diff --git a/crate/params/src/value_resolution_mode.rs b/crate/params/src/value_resolution_mode.rs index 262929051..e98e2cf1c 100644 --- a/crate/params/src/value_resolution_mode.rs +++ b/crate/params/src/value_resolution_mode.rs @@ -1,9 +1,14 @@ use serde::{Deserialize, Serialize}; /// When resolving `Value`s, whether to look up `Current` or `Goal`. -// -// Corresponds to marker types in `crate/data/src/marker.rs`. -// Remember to update there when updating here. +/// +/// # Design +/// +/// Remember to update these places when updating here. +/// +/// 1. Marker types in `crate/data/src/marker.rs`. +/// 2. `peace_params::MappingFnImpl`. +/// 3. Resource insertions in `ItemWrapper::setup`. // // TODO: Should we have modes for: // @@ -12,14 +17,21 @@ use serde::{Deserialize, Serialize}; // * `ExecutionBeginning` #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum ValueResolutionMode { - /// Resolve values using dry-applied states. - /// - /// The states in memory may be example / fake / placeholder values. - ApplyDry, + /// Resolve values using example states. + #[cfg(feature = "item_state_example")] + Example, + /// Resolve values using cleaned states. + Clean, /// Resolve values using current states. Current, /// Resolve values using goal states. Goal, - /// Resolve values using cleaned states. - Clean, + /// Resolve values using dry-applied states. + /// + /// The states in memory may be example / fake / placeholder values. + /// + /// TODO: resolve this in [#196] + /// + /// [#196]: https://github.com/azriel91/peace/issues/196 + ApplyDry, } diff --git a/crate/params_derive/src/impl_params_merge_ext_for_params.rs b/crate/params_derive/src/impl_params_merge_ext_for_params.rs new file mode 100644 index 000000000..936c43edb --- /dev/null +++ b/crate/params_derive/src/impl_params_merge_ext_for_params.rs @@ -0,0 +1,230 @@ +use syn::{ + punctuated::Punctuated, DeriveInput, Fields, Ident, ImplGenerics, Path, TypeGenerics, Variant, + WhereClause, +}; + +use crate::util::{ + field_name_partial, fields_deconstruct, fields_deconstruct_partial, is_phantom_data, + tuple_ident_from_field_index, variant_and_partial_match_arm, +}; + +/// `impl ParamsMergeExt for Params`, so that the framework can run +/// `params_example.merge(params_partial_current)` in `Item::try_state_*` +/// without needing to deconstruct the `Params::Partial`. +pub fn impl_params_merge_ext_for_params( + ast: &DeriveInput, + generics_split: &(ImplGenerics, TypeGenerics, Option<&WhereClause>), + peace_params_path: &Path, + params_name: &Ident, + params_partial_name: &Ident, +) -> proc_macro2::TokenStream { + let (impl_generics, ty_generics, where_clause) = generics_split; + + let params_merge_body = match &ast.data { + syn::Data::Struct(data_struct) => { + let fields = &data_struct.fields; + + struct_fields_merge_partial(params_name, params_partial_name, fields) + } + syn::Data::Enum(data_enum) => { + let variants = &data_enum.variants; + + variants_merge_partial(params_name, params_partial_name, variants) + } + syn::Data::Union(data_union) => { + let fields = Fields::from(data_union.fields.clone()); + + struct_fields_merge_partial(params_name, params_partial_name, &fields) + } + }; + + let mut generics_for_ref = ast.generics.clone(); + generics_for_ref.params.insert(0, parse_quote!('partial)); + + quote! { + impl #impl_generics #peace_params_path::ParamsMergeExt + for #params_name #ty_generics + #where_clause + { + fn merge(&mut self, params_partial: #params_partial_name #ty_generics) { + #params_merge_body + } + } + } +} + +fn struct_fields_merge_partial( + params_name: &Ident, + params_partial_name: &Ident, + fields: &Fields, +) -> proc_macro2::TokenStream { + let fields_deconstructed = fields_deconstruct(fields); + let fields_deconstructed_partial = fields_deconstruct_partial(fields); + let fields_merge_partial = fields_merge_partial(fields); + + match fields { + Fields::Named(_fields_named) => { + // Generates: + // + // ```rust + // let #params_partial_name { + // field_1, + // field_2, + // marker: PhantomData, + // } = self; + // + // let #params_partial_name { + // field_1: field_1_partial, + // field_2: field_2_partial, + // marker: PhantomData, + // } = params_partial; + // + // if let Some(field_1_partial) = field_1_partial { + // *field_1 = field_1_partial; + // } + // if let Some(field_2_partial) = field_2_partial { + // *field_2 = field_2_partial; + // } + // ``` + quote! { + let #params_name { + #(#fields_deconstructed),* + } = self; + let #params_partial_name { + #(#fields_deconstructed_partial),* + } = params_partial; + + #fields_merge_partial + } + } + Fields::Unnamed(_fields_unnamed) => { + // Generates: + // + // ```rust + // let #params_partial_name( + // _0, + // _1, + // PhantomData, + // ) = self; + // + // let #params_partial_name( + // _0_partial, + // _1_partial, + // PhantomData, + // ) = params_partial; + // + // if let Some(_0_partial) = _0_partial { + // *_0 = _0_partial; + // } + // if let Some(_1_partial) = _1_partial { + // *_1 = _1_partial; + // } + // ``` + quote! { + let #params_name(#(#fields_deconstructed),*) = self; + let #params_partial_name(#(#fields_deconstructed_partial),*) = params_partial; + + #fields_merge_partial + } + } + Fields::Unit => proc_macro2::TokenStream::new(), + } +} + +fn variants_merge_partial( + params_name: &Ident, + params_partial_name: &Ident, + variants: &Punctuated, +) -> proc_macro2::TokenStream { + // Generates: + // + // ```rust + // match (self, params_partial) { + // ( + // #params_name::Variant1, + // #params_partial_name::Variant1 + // ) => {} + // ( + // #params_name::Variant2(_0, _1, PhantomData), + // #params_partial_name::Variant2(_0_partial, _1_partial, PhantomData), + // ) => { + // if let Some(_0_partial) = _0_partial { + // *_0 = _0_partial; + // } + // if let Some(_1_partial) = _1_partial { + // *_1 = _1_partial; + // } + // } + // ( + // #params_name::Variant3 { + // field_1, + // field_2, + // marker: PhantomData, + // }, + // #params_partial_name::Variant3 { + // field_1: field_1_partial, + // field_2: field_2_partial, + // marker: PhantomData, + // }, + // ) => { + // if let Some(field_1_partial) = field_1_partial { + // *field_1 = field_1_partial; + // } + // if let Some(field_2_partial) = field_2_partial { + // *field_2 = field_2_partial; + // } + // } + // _ => {} // Merging different variants is not supported. + // } + // ``` + + let variant_merge_partial_arms = + variants + .iter() + .fold(proc_macro2::TokenStream::new(), |mut tokens, variant| { + let fields_merge_partial = fields_merge_partial(&variant.fields); + + tokens.extend(variant_and_partial_match_arm( + params_name, + params_partial_name, + variant, + fields_merge_partial, + )); + + tokens + }); + + quote! { + match (self, params_partial) { + #variant_merge_partial_arms + + _ => {} // Merging different variants is not supported. + } + } +} + +fn fields_merge_partial(fields: &Fields) -> proc_macro2::TokenStream { + fields + .iter() + .filter(|field| !is_phantom_data(&field.ty)) + .enumerate() + .map(|(field_index, field)| { + if let Some(field_ident) = field.ident.as_ref() { + let field_name_partial = field_name_partial(field_ident); + quote! { + if let Some(#field_name_partial) = #field_name_partial { + *#field_ident = #field_name_partial; + } + } + } else { + let field_ident = tuple_ident_from_field_index(field_index); + let field_name_partial = field_name_partial(&field_ident); + quote! { + if let Some(#field_name_partial) = #field_name_partial { + *#field_ident = #field_name_partial; + } + } + } + }) + .collect::() +} diff --git a/crate/params_derive/src/lib.rs b/crate/params_derive/src/lib.rs index 06c924d8d..7b44cd404 100644 --- a/crate/params_derive/src/lib.rs +++ b/crate/params_derive/src/lib.rs @@ -25,6 +25,7 @@ use crate::{ impl_field_wise_spec_rt_for_field_wise_external::impl_field_wise_spec_rt_for_field_wise_external, impl_from_params_for_params_field_wise::impl_from_params_for_params_field_wise, impl_from_params_for_params_partial::impl_from_params_for_params_partial, + impl_params_merge_ext_for_params::impl_params_merge_ext_for_params, impl_try_from_params_partial_for_params::impl_try_from_params_partial_for_params, impl_value_spec_rt_for_field_wise::impl_value_spec_rt_for_field_wise, type_gen::TypeGen, @@ -41,6 +42,7 @@ mod impl_field_wise_spec_rt_for_field_wise; mod impl_field_wise_spec_rt_for_field_wise_external; mod impl_from_params_for_params_field_wise; mod impl_from_params_for_params_partial; +mod impl_params_merge_ext_for_params; mod impl_try_from_params_partial_for_params; mod impl_value_spec_rt_for_field_wise; mod spec_is_usable; @@ -57,10 +59,17 @@ mod util; /// references the `peace_params` crate instead of the `peace::params` /// re-export. /// -/// For types derived from `struct` `Param`s -- `Spec`, `Partial` -- we also: +/// # Generated Types /// -/// * Generate getters and mut getters for non-`pub`, non-`PhantomData` fields. -/// * Generate a constructor if not all fields are `pub`. +/// * `*ParamsSpec`: Similar to `Params`, with each field taking in a +/// specification of how the value is set. +/// * `*ParamsPartial`: Similar to `Params`, with each field wrapped in +/// `Option`. +/// +/// Both of these generated types will have: +/// +/// * A constructor if not all fields are `pub`. +/// * Getters and mut getters for non-`pub`, non-`PhantomData` fields. /// /// Maybe we should also generate a `SpecBuilder` -- see commit `10f63611` which /// removed builder generation. @@ -233,7 +242,7 @@ fn impl_value(ast: &mut DeriveInput, impl_mode: ImplMode) -> proc_macro2::TokenS quote!(#ty_generics) }; - let (t_partial, t_field_wise, t_field_wise_builder) = + let (t_partial, t_field_wise, t_field_wise_builder, impl_params_merge_ext_for_params) = if is_fieldless_type(ast) || impl_mode == ImplMode::Fieldless { let ty_generics = &generics_split.1; let value_ty: Type = parse_quote!(#value_name #ty_generics); @@ -249,7 +258,7 @@ fn impl_value(ast: &mut DeriveInput, impl_mode: ImplMode) -> proc_macro2::TokenS &t_partial_name, ); - (t_partial, t_field_wise, None) + (t_partial, t_field_wise, None, None) } else { let t_partial = t_partial(ast, &generics_split, value_name, &t_partial_name); let t_field_wise = t_field_wise( @@ -271,8 +280,20 @@ fn impl_value(ast: &mut DeriveInput, impl_mode: ImplMode) -> proc_macro2::TokenS impl_mode, &field_wise_enum_builder_ctx, ); + let impl_params_merge_ext_for_params = impl_params_merge_ext_for_params( + ast, + &generics_split, + &peace_params_path, + value_name, + &t_partial_name, + ); - (t_partial, t_field_wise, Some(t_field_wise_builder)) + ( + t_partial, + t_field_wise, + Some(t_field_wise_builder), + Some(impl_params_merge_ext_for_params), + ) }; let (impl_generics, ty_generics, where_clause) = &generics_split; @@ -310,6 +331,8 @@ fn impl_value(ast: &mut DeriveInput, impl_mode: ImplMode) -> proc_macro2::TokenS #t_field_wise #t_field_wise_builder + + #impl_params_merge_ext_for_params }); impl_value_tokens diff --git a/crate/params_derive/src/util.rs b/crate/params_derive/src/util.rs index 81fd00ebb..2a413312a 100644 --- a/crate/params_derive/src/util.rs +++ b/crate/params_derive/src/util.rs @@ -245,6 +245,11 @@ pub fn is_phantom_data(field_ty: &Type) -> bool { if matches!(path.segments.last(), Some(segment) if segment.ident == "PhantomData")) } +/// Returns idents as `field_name_partial`. +pub fn field_name_partial(field_name: &Ident) -> Ident { + format_ident!("{}_partial", field_name) +} + /// Returns tuple idents as `_n` where `n` is the index of the field. pub fn tuple_ident_from_field_index(field_index: usize) -> Ident { Ident::new(&format!("_{field_index}"), Span::call_site()) @@ -265,6 +270,25 @@ pub fn fields_deconstruct(fields: &Fields) -> Vec { fields_deconstruct_retain(fields, false) } +/// Returns a comma separated list of deconstructed fields, with a `_partial` +/// suffix. +/// +/// Named fields are returned as `field_name_partial`, whose type is +/// `Option`. +/// +/// Tuple fields are returned as `_n_partial`, and marker fields are returned as +/// `::std::marker::PhantomData`. +pub fn fields_deconstruct_partial(fields: &Fields) -> Vec { + fields_deconstruct_retain_map( + fields, + false, + Some(|field_name| { + let field_name_partial = field_name_partial(field_name); + quote!(#field_name_partial) + }), + ) +} + /// Returns a comma separated list of deconstructed fields, deconstructed as /// `field: Some(field)`. /// @@ -581,6 +605,55 @@ pub fn variant_match_arm( } } +/// Generates an enum variant match arm. +/// +/// # Parameters +/// +/// * `enum_name`: e.g. `MyParams` +/// * `enum_partial_name`: e.g. `MyParamsPartial` +/// * `variant`: Variant to generate the match arm for. +/// * `match_arm_body`: Tokens to insert as the match arm body. +pub fn variant_and_partial_match_arm( + enum_name: &Ident, + enum_partial_name: &Ident, + variant: &Variant, + match_arm_body: proc_macro2::TokenStream, +) -> proc_macro2::TokenStream { + let variant_name = &variant.ident; + let fields_deconstructed = fields_deconstruct(&variant.fields); + let fields_deconstructed_partial = fields_deconstruct_partial(&variant.fields); + match &variant.fields { + Fields::Named(_fields_named) => { + quote! { + ( + #enum_name::#variant_name { #(#fields_deconstructed),* }, + #enum_partial_name::#variant_name { #(#fields_deconstructed_partial),* }, + ) => { + #match_arm_body + } + } + } + Fields::Unnamed(_) => { + quote! { + ( + #enum_name::#variant_name(#(#fields_deconstructed),*), + #enum_partial_name::#variant_name(#(#fields_deconstructed_partial),*), + ) => { + #match_arm_body + } + } + } + Fields::Unit => { + quote! { + ( + #enum_name::#variant_name, + #enum_partial_name::#variant_name + ) => {} + } + } + } +} + /// Returns the reference `&Field` for any `Field`. /// /// This includes special handling for the following types: diff --git a/crate/resource_rt/src/resources.rs b/crate/resource_rt/src/resources.rs index d8ca41f37..077c217df 100644 --- a/crate/resource_rt/src/resources.rs +++ b/crate/resource_rt/src/resources.rs @@ -7,7 +7,8 @@ use crate::resources::ts::{Empty, SetUp}; pub mod ts; -/// Map of all types at runtime. [`resman::Resources`] newtype. +/// Runtime borrow-checked typemap of data available to the command context. +/// [`resman::Resources`] newtype. /// /// This augments the any-map functionality of [`resman::Resources`] with type /// state, so that it is impossible for developers to pass `Resources` to diff --git a/crate/rt/src/cmd_blocks/apply_exec_cmd_block.rs b/crate/rt/src/cmd_blocks/apply_exec_cmd_block.rs index b7ed4daa7..793cdc31b 100644 --- a/crate/rt/src/cmd_blocks/apply_exec_cmd_block.rs +++ b/crate/rt/src/cmd_blocks/apply_exec_cmd_block.rs @@ -33,6 +33,7 @@ cfg_if::cfg_if! { use peace_cfg::{ progress::{ + CmdBlockItemInteractionType, CmdProgressUpdate, ProgressComplete, ProgressMsgUpdate, @@ -404,7 +405,7 @@ where states_applied_mut.insert_raw(item_id.clone(), state_applied); } - // Save `state_target` (which is state_target) if we are not cleaning + // Save `state_target` (which is `state_goal`) if we are not cleaning // up. match apply_for { ApplyFor::Ensure => { @@ -430,6 +431,11 @@ where type InputT = (StatesCurrent, States); type Outcome = (StatesPrevious, States, States); + #[cfg(feature = "output_progress")] + fn cmd_block_item_interaction_type(&self) -> CmdBlockItemInteractionType { + CmdBlockItemInteractionType::Write + } + fn input_fetch( &self, resources: &mut Resources, diff --git a/crate/rt/src/cmd_blocks/apply_state_sync_check_cmd_block.rs b/crate/rt/src/cmd_blocks/apply_state_sync_check_cmd_block.rs index 1ea5fe396..5aec249ae 100644 --- a/crate/rt/src/cmd_blocks/apply_state_sync_check_cmd_block.rs +++ b/crate/rt/src/cmd_blocks/apply_state_sync_check_cmd_block.rs @@ -15,6 +15,7 @@ cfg_if::cfg_if! { if #[cfg(feature = "output_progress")] { use peace_cfg::{ progress::{ + CmdBlockItemInteractionType, CmdProgressUpdate, ProgressComplete, ProgressDelta, @@ -228,6 +229,11 @@ where type InputT = (); type Outcome = Self::InputT; + #[cfg(feature = "output_progress")] + fn cmd_block_item_interaction_type(&self) -> CmdBlockItemInteractionType { + CmdBlockItemInteractionType::Read + } + fn input_fetch(&self, _resources: &mut Resources) -> Result<(), ResourceFetchError> { Ok(()) } @@ -265,6 +271,11 @@ where type InputT = (StatesCurrentStored, StatesCurrent); type Outcome = Self::InputT; + #[cfg(feature = "output_progress")] + fn cmd_block_item_interaction_type(&self) -> CmdBlockItemInteractionType { + CmdBlockItemInteractionType::Read + } + fn input_fetch( &self, resources: &mut Resources, @@ -339,6 +350,11 @@ where type InputT = (StatesGoalStored, StatesGoal); type Outcome = Self::InputT; + #[cfg(feature = "output_progress")] + fn cmd_block_item_interaction_type(&self) -> CmdBlockItemInteractionType { + CmdBlockItemInteractionType::Read + } + fn input_fetch( &self, resources: &mut Resources, @@ -419,6 +435,11 @@ where ); type Outcome = Self::InputT; + #[cfg(feature = "output_progress")] + fn cmd_block_item_interaction_type(&self) -> CmdBlockItemInteractionType { + CmdBlockItemInteractionType::Read + } + fn input_fetch( &self, resources: &mut Resources, diff --git a/crate/rt/src/cmd_blocks/diff_cmd_block.rs b/crate/rt/src/cmd_blocks/diff_cmd_block.rs index 3518fe615..a9c5980d4 100644 --- a/crate/rt/src/cmd_blocks/diff_cmd_block.rs +++ b/crate/rt/src/cmd_blocks/diff_cmd_block.rs @@ -26,7 +26,7 @@ use crate::cmds::DiffStateSpec; cfg_if::cfg_if! { if #[cfg(feature = "output_progress")] { - use peace_cfg::progress::CmdProgressUpdate; + use peace_cfg::progress::{CmdBlockItemInteractionType, CmdProgressUpdate}; use tokio::sync::mpsc::Sender; } } @@ -122,6 +122,11 @@ where type InputT = (States, States); type Outcome = (StateDiffs, Self::InputT); + #[cfg(feature = "output_progress")] + fn cmd_block_item_interaction_type(&self) -> CmdBlockItemInteractionType { + CmdBlockItemInteractionType::Local + } + fn input_fetch( &self, resources: &mut Resources, diff --git a/crate/rt/src/cmd_blocks/states_clean_insertion_cmd_block.rs b/crate/rt/src/cmd_blocks/states_clean_insertion_cmd_block.rs index aba474f2b..7f719ea9f 100644 --- a/crate/rt/src/cmd_blocks/states_clean_insertion_cmd_block.rs +++ b/crate/rt/src/cmd_blocks/states_clean_insertion_cmd_block.rs @@ -15,7 +15,7 @@ use peace_rt_model_core::IndexMap; cfg_if::cfg_if! { if #[cfg(feature = "output_progress")] { - use peace_cfg::progress::CmdProgressUpdate; + use peace_cfg::progress::{CmdBlockItemInteractionType, CmdProgressUpdate}; use tokio::sync::mpsc::Sender; } } @@ -52,6 +52,11 @@ where type InputT = (); type Outcome = StatesClean; + #[cfg(feature = "output_progress")] + fn cmd_block_item_interaction_type(&self) -> CmdBlockItemInteractionType { + CmdBlockItemInteractionType::Local + } + fn input_fetch(&self, _resources: &mut Resources) -> Result<(), ResourceFetchError> { Ok(()) } diff --git a/crate/rt/src/cmd_blocks/states_current_read_cmd_block.rs b/crate/rt/src/cmd_blocks/states_current_read_cmd_block.rs index 038c30119..bf05613ab 100644 --- a/crate/rt/src/cmd_blocks/states_current_read_cmd_block.rs +++ b/crate/rt/src/cmd_blocks/states_current_read_cmd_block.rs @@ -15,7 +15,7 @@ use peace_rt_model::{StatesSerializer, Storage}; cfg_if::cfg_if! { if #[cfg(feature = "output_progress")] { - use peace_cfg::progress::CmdProgressUpdate; + use peace_cfg::progress::{CmdBlockItemInteractionType, CmdProgressUpdate}; use tokio::sync::mpsc::Sender; } } @@ -81,6 +81,11 @@ where type InputT = (); type Outcome = StatesCurrentStored; + #[cfg(feature = "output_progress")] + fn cmd_block_item_interaction_type(&self) -> CmdBlockItemInteractionType { + CmdBlockItemInteractionType::Local + } + fn input_fetch(&self, _resources: &mut Resources) -> Result<(), ResourceFetchError> { Ok(()) } diff --git a/crate/rt/src/cmd_blocks/states_discover_cmd_block.rs b/crate/rt/src/cmd_blocks/states_discover_cmd_block.rs index 25ff4c0b9..a918330e3 100644 --- a/crate/rt/src/cmd_blocks/states_discover_cmd_block.rs +++ b/crate/rt/src/cmd_blocks/states_discover_cmd_block.rs @@ -25,6 +25,7 @@ cfg_if::cfg_if! { if #[cfg(feature = "output_progress")] { use peace_cfg::{ progress::{ + CmdBlockItemInteractionType, CmdProgressUpdate, ProgressComplete, ProgressDelta, @@ -266,6 +267,11 @@ where type InputT = (); type Outcome = States; + #[cfg(feature = "output_progress")] + fn cmd_block_item_interaction_type(&self) -> CmdBlockItemInteractionType { + CmdBlockItemInteractionType::Read + } + fn input_fetch(&self, _resources: &mut Resources) -> Result<(), ResourceFetchError> { Ok(()) } @@ -409,6 +415,11 @@ where type InputT = (); type Outcome = States; + #[cfg(feature = "output_progress")] + fn cmd_block_item_interaction_type(&self) -> CmdBlockItemInteractionType { + CmdBlockItemInteractionType::Read + } + fn input_fetch(&self, _resources: &mut Resources) -> Result<(), ResourceFetchError> { Ok(()) } @@ -552,6 +563,11 @@ where type InputT = (); type Outcome = (States, States); + #[cfg(feature = "output_progress")] + fn cmd_block_item_interaction_type(&self) -> CmdBlockItemInteractionType { + CmdBlockItemInteractionType::Read + } + fn input_fetch(&self, _resources: &mut Resources) -> Result<(), ResourceFetchError> { Ok(()) } diff --git a/crate/rt/src/cmd_blocks/states_goal_read_cmd_block.rs b/crate/rt/src/cmd_blocks/states_goal_read_cmd_block.rs index 439ff095e..a93de8970 100644 --- a/crate/rt/src/cmd_blocks/states_goal_read_cmd_block.rs +++ b/crate/rt/src/cmd_blocks/states_goal_read_cmd_block.rs @@ -15,7 +15,7 @@ use peace_rt_model::{StatesSerializer, Storage}; cfg_if::cfg_if! { if #[cfg(feature = "output_progress")] { - use peace_cfg::progress::CmdProgressUpdate; + use peace_cfg::progress::{CmdBlockItemInteractionType, CmdProgressUpdate}; use tokio::sync::mpsc::Sender; } } @@ -81,6 +81,11 @@ where type InputT = (); type Outcome = StatesGoalStored; + #[cfg(feature = "output_progress")] + fn cmd_block_item_interaction_type(&self) -> CmdBlockItemInteractionType { + CmdBlockItemInteractionType::Local + } + fn input_fetch(&self, _resources: &mut Resources) -> Result<(), ResourceFetchError> { Ok(()) } diff --git a/crate/rt_model/Cargo.toml b/crate/rt_model/Cargo.toml index 9a53d32bd..84e484f76 100644 --- a/crate/rt_model/Cargo.toml +++ b/crate/rt_model/Cargo.toml @@ -24,20 +24,23 @@ cfg-if = { workspace = true } dyn-clone = { workspace = true } erased-serde = { workspace = true } futures = { workspace = true } +heck = { workspace = true, optional = true } +indexmap = { workspace = true, optional = true } 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_item_model = { workspace = true, optional = true } peace_params = { workspace = true } peace_resource_rt = { workspace = true } peace_rt_model_core = { workspace = true } peace_rt_model_hack = { workspace = true, optional = true } serde = { workspace = true } serde_yaml = { workspace = true } -type_reg = { workspace = true, features = ["resman"] } tynm = { workspace = true } +type_reg = { workspace = true, features = ["resman"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] peace_rt_model_native = { workspace = true } @@ -54,6 +57,20 @@ error_reporting = [ ] output_in_memory = ["peace_rt_model_native/output_in_memory"] output_progress = [ + "dep:peace_item_model", "peace_cfg/output_progress", + "peace_item_model/output_progress", "peace_rt_model_hack/output_progress" ] +item_interactions = [ + "dep:heck", + "dep:indexmap", + "dep:peace_item_model", + "peace_cfg/item_interactions", + "peace_item_model/item_locations_and_interactions", +] +item_state_example = [ + "peace_cfg/item_state_example", + "peace_data/item_state_example", + "peace_params/item_state_example", +] diff --git a/crate/rt_model/src/flow.rs b/crate/rt_model/src/flow.rs index 9a804b64c..bdeb90786 100644 --- a/crate/rt_model/src/flow.rs +++ b/crate/rt_model/src/flow.rs @@ -4,6 +4,31 @@ use peace_flow_model::{FlowSpecInfo, ItemSpecInfo}; use crate::ItemGraph; +cfg_if::cfg_if! { + if #[cfg(all(feature = "item_interactions", feature = "item_state_example"))] { + use std::collections::{BTreeMap, BTreeSet}; + + use indexmap::IndexMap; + use peace_cfg::ItemId; + use peace_item_model::{ + ItemInteraction, + ItemInteractionsCurrentOrExample, + ItemLocation, + ItemLocationsAndInteractions, + ItemLocationTree, + }; + use peace_params::ParamsSpecs; + use peace_resource_rt::{resources::ts::SetUp, Resources}; + } +} + +#[cfg(all( + feature = "item_interactions", + feature = "item_state_example", + feature = "output_progress", +))] +use std::collections::{HashMap, HashSet}; + /// A flow to manage items. /// /// A Flow ID is strictly associated with an [`ItemGraph`], as the graph @@ -74,4 +99,490 @@ impl Flow { FlowSpecInfo::new(flow_id, graph_info) } + + // TODO: Refactor -- there is a lot of duplication between this method and + // `item_locations_and_interactions_current` + #[cfg(all(feature = "item_interactions", feature = "item_state_example"))] + pub fn item_locations_and_interactions_example( + &self, + params_specs: &ParamsSpecs, + resources: &Resources, + ) -> ItemLocationsAndInteractions + where + E: 'static, + { + // Build the flattened hierarchy. + // + // Regardless of how nested each `ItemLocation` is, the map will have an entry + // for it. + // + // The entry key is the `ItemLocation`, and the values are a list of its direct + // descendent `ItemLocation`s. + // + // This means a lot of cloning of `ItemLocation`s. + + let item_interactions_ctx = ItemInteractionsCtx { + item_location_direct_descendents: BTreeMap::new(), + item_to_item_interactions: IndexMap::with_capacity(self.graph().node_count()), + // Rough estimate that each item has about 4 item locations + // + // * 2 `ItemLocationAncestors`s for from, 2 for to. + // * Some may have more, but we also combine `ItemLocation`s. + // + // After the `ItemLocationTree`s are constructed, we'll have an accurate + // number, but they are being constructed at the same time as this map. + #[cfg(feature = "output_progress")] + item_location_to_item_id_sets: HashMap::with_capacity(self.graph().node_count() * 4), + }; + let item_interactions_ctx = self + .graph() + .iter() + // Note: This will silently drop the item locations if `interactions_example` fails to + // return. + .filter_map(|item| { + item.interactions_example(params_specs, resources) + .ok() + .map(|item_interactions_example| (item.id(), item_interactions_example)) + }) + .fold( + item_interactions_ctx, + |item_interactions_ctx, (item_id, item_interactions_example)| { + let ItemInteractionsCtx { + mut item_location_direct_descendents, + mut item_to_item_interactions, + #[cfg(feature = "output_progress")] + mut item_location_to_item_id_sets, + } = item_interactions_ctx; + + item_location_descendents_populate( + &item_interactions_example, + &mut item_location_direct_descendents, + ); + + #[cfg(feature = "output_progress")] + item_interactions_example + .iter() + .for_each(|item_interaction| match &item_interaction { + ItemInteraction::Push(item_interaction_push) => item_interaction_push + .location_from() + .iter() + .last() + .into_iter() + .chain(item_interaction_push.location_to().iter().last()) + .for_each(|item_location| { + item_location_to_item_id_sets_insert( + &mut item_location_to_item_id_sets, + item_location, + item_id, + ) + }), + ItemInteraction::Pull(item_interaction_pull) => item_interaction_pull + .location_client() + .iter() + .last() + .into_iter() + .chain(item_interaction_pull.location_server().iter().last()) + .for_each(|item_location| { + item_location_to_item_id_sets_insert( + &mut item_location_to_item_id_sets, + item_location, + item_id, + ) + }), + ItemInteraction::Within(item_interaction_within) => { + item_interaction_within + .location() + .iter() + .last() + .into_iter() + .for_each(|item_location| { + item_location_to_item_id_sets_insert( + &mut item_location_to_item_id_sets, + item_location, + item_id, + ) + }) + } + }); + + item_to_item_interactions + .insert(item_id.clone(), item_interactions_example.into_inner()); + + ItemInteractionsCtx { + item_location_direct_descendents, + item_to_item_interactions, + #[cfg(feature = "output_progress")] + item_location_to_item_id_sets, + } + }, + ); + + let ItemInteractionsCtx { + item_location_direct_descendents, + item_to_item_interactions, + #[cfg(feature = "output_progress")] + item_location_to_item_id_sets, + } = item_interactions_ctx; + + let item_locations_top_level = item_location_direct_descendents + .keys() + .filter(|item_location| { + // this item_location is not in any descendents + !item_location_direct_descendents + .values() + .any(|item_location_descendents| { + item_location_descendents.contains(item_location) + }) + }) + .cloned() + .collect::>(); + + let item_locations_top_level_len = item_locations_top_level.len(); + let (_item_location_direct_descendents, item_location_trees) = + item_locations_top_level.into_iter().fold( + ( + item_location_direct_descendents, + Vec::with_capacity(item_locations_top_level_len), + ), + |(mut item_location_direct_descendents, mut item_location_trees), item_location| { + let item_location_tree = item_location_tree_collect( + &mut item_location_direct_descendents, + item_location, + ); + item_location_trees.push(item_location_tree); + + (item_location_direct_descendents, item_location_trees) + }, + ); + + let item_location_count = item_location_trees.iter().fold( + item_location_trees.len(), + |item_location_count_acc, item_location_tree| { + item_location_count_acc + item_location_tree.item_location_count() + }, + ); + + ItemLocationsAndInteractions::new( + item_location_trees, + item_to_item_interactions, + item_location_count, + #[cfg(feature = "output_progress")] + item_location_to_item_id_sets, + ) + } + + // TODO: Refactor -- there is a lot of duplication between this method and + // `item_locations_and_interactions_example` + #[cfg(all(feature = "item_interactions", feature = "item_state_example"))] + pub fn item_locations_and_interactions_current( + &self, + params_specs: &ParamsSpecs, + resources: &Resources, + ) -> ItemLocationsAndInteractions + where + E: 'static, + { + // Build the flattened hierarchy. + // + // Regardless of how nested each `ItemLocation` is, the map will have an entry + // for it. + // + // The entry key is the `ItemLocation`, and the values are a list of its direct + // descendent `ItemLocation`s. + // + // This means a lot of cloning of `ItemLocation`s. + + let item_interactions_ctx = ItemInteractionsCtx { + item_location_direct_descendents: BTreeMap::new(), + item_to_item_interactions: IndexMap::with_capacity(self.graph().node_count()), + // Rough estimate that each item has about 4 item locations + // + // * 2 `ItemLocationAncestors`s for from, 2 for to. + // * Some may have more, but we also combine `ItemLocation`s. + // + // After the `ItemLocationTree`s are constructed, we'll have an accurate + // number, but they are being constructed at the same time as this map. + #[cfg(feature = "output_progress")] + item_location_to_item_id_sets: HashMap::with_capacity(self.graph().node_count() * 4), + }; + let item_interactions_ctx = self + .graph() + .iter() + // Note: This will silently drop the item locations if `interactions_try_current` fails + // to return. + .filter_map(|item| { + item.interactions_try_current(params_specs, resources) + .ok() + .map(|item_interactions_current_or_example| { + (item.id(), item_interactions_current_or_example) + }) + }) + .fold( + item_interactions_ctx, + |item_interactions_ctx, (item_id, item_interactions_current_or_example)| { + let ItemInteractionsCtx { + mut item_location_direct_descendents, + mut item_to_item_interactions, + #[cfg(feature = "output_progress")] + mut item_location_to_item_id_sets, + } = item_interactions_ctx; + + // TODO: we need to hide the nodes if they came from `Example`. + let item_interactions_current_or_example = + match item_interactions_current_or_example { + ItemInteractionsCurrentOrExample::Current( + item_interactions_current, + ) => item_interactions_current.into_inner(), + ItemInteractionsCurrentOrExample::Example( + item_interactions_example, + ) => item_interactions_example.into_inner(), + }; + + item_location_descendents_populate( + &item_interactions_current_or_example, + &mut item_location_direct_descendents, + ); + + #[cfg(feature = "output_progress")] + item_interactions_current_or_example + .iter() + .for_each(|item_interaction| match &item_interaction { + ItemInteraction::Push(item_interaction_push) => item_interaction_push + .location_from() + .iter() + .last() + .into_iter() + .chain(item_interaction_push.location_to().iter().last()) + .for_each(|item_location| { + item_location_to_item_id_sets_insert( + &mut item_location_to_item_id_sets, + item_location, + item_id, + ) + }), + ItemInteraction::Pull(item_interaction_pull) => item_interaction_pull + .location_client() + .iter() + .last() + .into_iter() + .chain(item_interaction_pull.location_server().iter().last()) + .for_each(|item_location| { + item_location_to_item_id_sets_insert( + &mut item_location_to_item_id_sets, + item_location, + item_id, + ) + }), + ItemInteraction::Within(item_interaction_within) => { + item_interaction_within + .location() + .iter() + .last() + .into_iter() + .for_each(|item_location| { + item_location_to_item_id_sets_insert( + &mut item_location_to_item_id_sets, + item_location, + item_id, + ) + }) + } + }); + + item_to_item_interactions + .insert(item_id.clone(), item_interactions_current_or_example); + + ItemInteractionsCtx { + item_location_direct_descendents, + item_to_item_interactions, + #[cfg(feature = "output_progress")] + item_location_to_item_id_sets, + } + }, + ); + + let ItemInteractionsCtx { + item_location_direct_descendents, + item_to_item_interactions, + #[cfg(feature = "output_progress")] + item_location_to_item_id_sets, + } = item_interactions_ctx; + + let item_locations_top_level = item_location_direct_descendents + .keys() + .filter(|item_location| { + // this item_location is not in any descendents + !item_location_direct_descendents + .values() + .any(|item_location_descendents| { + item_location_descendents.contains(item_location) + }) + }) + .cloned() + .collect::>(); + + let item_locations_top_level_len = item_locations_top_level.len(); + let (_item_location_direct_descendents, item_location_trees) = + item_locations_top_level.into_iter().fold( + ( + item_location_direct_descendents, + Vec::with_capacity(item_locations_top_level_len), + ), + |(mut item_location_direct_descendents, mut item_location_trees), item_location| { + let item_location_tree = item_location_tree_collect( + &mut item_location_direct_descendents, + item_location, + ); + item_location_trees.push(item_location_tree); + + (item_location_direct_descendents, item_location_trees) + }, + ); + + let item_location_count = item_location_trees.iter().fold( + item_location_trees.len(), + |item_location_count_acc, item_location_tree| { + item_location_count_acc + item_location_tree.item_location_count() + }, + ); + + ItemLocationsAndInteractions::new( + item_location_trees, + item_to_item_interactions, + item_location_count, + #[cfg(feature = "output_progress")] + item_location_to_item_id_sets, + ) + } +} + +#[cfg(all( + feature = "item_interactions", + feature = "item_state_example", + feature = "output_progress", +))] +fn item_location_to_item_id_sets_insert( + item_location_to_item_id_sets: &mut HashMap>, + item_location: &ItemLocation, + item_id: &ItemId, +) { + if let Some(item_id_set) = item_location_to_item_id_sets.get_mut(item_location) { + item_id_set.insert(item_id.clone()); + } else { + let mut item_id_set = HashSet::new(); + item_id_set.insert(item_id.clone()); + item_location_to_item_id_sets.insert(item_location.clone(), item_id_set); + } +} + +#[cfg(all(feature = "item_interactions", feature = "item_state_example",))] +fn item_location_descendents_populate( + item_interactions_current_or_example: &[ItemInteraction], + item_location_direct_descendents: &mut BTreeMap>, +) { + item_interactions_current_or_example.iter().for_each( + |item_interaction| match &item_interaction { + ItemInteraction::Push(item_interaction_push) => { + item_location_descendents_insert( + item_location_direct_descendents, + item_interaction_push.location_from(), + ); + item_location_descendents_insert( + item_location_direct_descendents, + item_interaction_push.location_to(), + ); + } + ItemInteraction::Pull(item_interaction_pull) => { + item_location_descendents_insert( + item_location_direct_descendents, + item_interaction_pull.location_client(), + ); + item_location_descendents_insert( + item_location_direct_descendents, + item_interaction_pull.location_server(), + ); + } + ItemInteraction::Within(item_interaction_within) => { + item_location_descendents_insert( + item_location_direct_descendents, + item_interaction_within.location(), + ); + } + }, + ); +} + +/// Recursively constructs an `ItemLocationTree`. +#[cfg(all(feature = "item_interactions", feature = "item_state_example"))] +fn item_location_tree_collect( + item_location_direct_descendents: &mut BTreeMap>, + item_location_parent: ItemLocation, +) -> ItemLocationTree { + match item_location_direct_descendents.remove_entry(&item_location_parent) { + Some((item_location, item_location_children)) => { + let children = item_location_children + .into_iter() + .map(|item_location_child| { + item_location_tree_collect( + item_location_direct_descendents, + item_location_child, + ) + }) + .collect::>(); + ItemLocationTree::new(item_location, children) + } + + // Should never be reached. + None => ItemLocationTree::new(item_location_parent, Vec::new()), + } +} + +/// Inserts / extends the `item_location_direct_descendents` with an entry for +/// each `ItemLocation` and its direct `ItemLocation` descendents. +#[cfg(all(feature = "item_interactions", feature = "item_state_example"))] +fn item_location_descendents_insert( + item_location_direct_descendents: &mut BTreeMap>, + item_location_ancestors: &[ItemLocation], +) { + // Each subsequent `ItemLocation` in `location_from` is a child of the previous + // `ItemLocation`. + let item_location_iter = item_location_ancestors.iter(); + let item_location_child_iter = item_location_ancestors.iter().skip(1); + + item_location_iter.zip(item_location_child_iter).for_each( + |(item_location, item_location_child)| { + // Save one clone by not using `BTreeSet::entry` + if let Some(item_location_children) = + item_location_direct_descendents.get_mut(item_location) + { + if !item_location_children.contains(item_location_child) { + item_location_children.insert(item_location_child.clone()); + } + } else { + let mut item_location_children = BTreeSet::new(); + item_location_children.insert(item_location_child.clone()); + item_location_direct_descendents + .insert(item_location.clone(), item_location_children); + } + + // Add an empty set for the child `ItemLocation`. + if !item_location_direct_descendents.contains_key(item_location_child) { + item_location_direct_descendents + .insert(item_location_child.clone(), BTreeSet::new()); + } + }, + ); +} + +/// Accumulates the links between +#[cfg(all(feature = "item_interactions", feature = "item_state_example"))] +struct ItemInteractionsCtx { + /// Map from each `ItemLocation` to all of its direct descendents collected + /// from all items. + item_location_direct_descendents: BTreeMap>, + /// Map from each item to each of its `ItemInteractions`. + item_to_item_interactions: IndexMap>, + /// Tracks the items that referred to this item location. + #[cfg(feature = "output_progress")] + item_location_to_item_id_sets: HashMap>, } diff --git a/crate/rt_model/src/in_memory_text_output.rs b/crate/rt_model/src/in_memory_text_output.rs index b2f5f31b9..54e7efa04 100644 --- a/crate/rt_model/src/in_memory_text_output.rs +++ b/crate/rt_model/src/in_memory_text_output.rs @@ -5,7 +5,11 @@ use crate::Error; cfg_if::cfg_if! { if #[cfg(feature = "output_progress")] { - use peace_cfg::progress::{ProgressTracker, ProgressUpdateAndId}; + use peace_cfg::{ + progress::{CmdBlockItemInteractionType, ProgressTracker, ProgressUpdateAndId}, + ItemId, + }; + use peace_item_model::ItemLocationState; use crate::CmdProgressTracker; } @@ -51,6 +55,21 @@ where ) { } + #[cfg(feature = "output_progress")] + async fn cmd_block_start( + &mut self, + _cmd_block_item_interaction_type: CmdBlockItemInteractionType, + ) { + } + + #[cfg(feature = "output_progress")] + async fn item_location_state( + &mut self, + _item_id: ItemId, + _item_location_state: ItemLocationState, + ) { + } + #[cfg(feature = "output_progress")] async fn progress_end(&mut self, _cmd_progress_tracker: &CmdProgressTracker) {} diff --git a/crate/rt_model/src/item_boxed.rs b/crate/rt_model/src/item_boxed.rs index 5c652512f..fb1c9e3c6 100644 --- a/crate/rt_model/src/item_boxed.rs +++ b/crate/rt_model/src/item_boxed.rs @@ -17,7 +17,7 @@ use std::{ use peace_cfg::Item; use peace_data::fn_graph::{DataAccessDyn, TypeIds}; -use peace_params::Params; +use peace_params::{Params, ParamsMergeExt}; use crate::{ItemRt, ItemWrapper}; @@ -76,7 +76,12 @@ where + From + 'static, for<'params> ::Params<'params>: - TryFrom<<::Params<'params> as Params>::Partial>, + ParamsMergeExt + TryFrom<<::Params<'params> as Params>::Partial>, + for<'params> <::Params<'params> as Params>::Partial: From< + <::Params<'params> as TryFrom< + <::Params<'params> as Params>::Partial, + >>::Error, + >, for<'params> as Params>::Partial: From>, { fn from(item: I) -> Self { diff --git a/crate/rt_model/src/item_rt.rs b/crate/rt_model/src/item_rt.rs index 9c1864966..fed70d631 100644 --- a/crate/rt_model/src/item_rt.rs +++ b/crate/rt_model/src/item_rt.rs @@ -76,6 +76,50 @@ pub trait ItemRt: where E: Debug + std::error::Error; + /// Returns an example fully deployed state of the managed item. + /// + /// # Design + /// + /// This is *expected* to always return a value, as it is used to: + /// + /// * Display a diagram that shows the user what the item looks like when it + /// is fully deployed, without actually interacting with any external + /// state. + /// + /// As much as possible, use the values in the provided params and data. + /// + /// This function should **NOT** interact with any external services, or + /// read from files that are part of the automation process, e.g. + /// querying data from a web endpoint, or reading files that may be + /// downloaded by a predecessor. + /// + /// ## Fallibility + /// + /// [`Item::state_example`] is deliberately infallible to signal to + /// implementors that calling an external service / read from a file is + /// incorrect implementation for this method -- values in params / data + /// may be example values from other items that may not resolve. + /// + /// [`ItemRt::state_example`] *is* fallible as value resolution for + /// parameters may fail, e.g. if there is a bug in Peace, or an item's + /// parameters requests a type that doesn't exist in [`Resources`]. + /// + /// ## Non-async + /// + /// This signals to implementors that this function should be a cheap + /// example state computation that is relatively realistic rather than + /// determining an accurate value. + /// + /// [`Item::state_example`]: peace_cfg::Item::Item::state_example + #[cfg(feature = "item_state_example")] + fn state_example( + &self, + params_specs: &ParamsSpecs, + resources: &Resources, + ) -> Result + where + E: Debug + std::error::Error; + /// Runs [`Item::state_clean`]. /// /// [`Item::state_clean`]: peace_cfg::Item::state_clean @@ -251,4 +295,70 @@ pub trait ItemRt: ) -> Result<(), E> where E: Debug + std::error::Error; + + /// Returns the physical resources that this item interacts with, purely + /// using example state. + /// + /// # Design + /// + /// This method returns interactions from [`Item::interactions`], passing in + /// parameters computed from example state. + /// + /// ## Fallibility + /// + /// [`Item::interactions`] is infallible as computing `ItemInteractions` + /// should purely be instantiating objects. + /// + /// [`ItemRt::interactions_example`] *is* fallible as value resolution for + /// parameters may fail, e.g. if there is a bug in Peace, or an item's + /// parameters requests a type that doesn't exist in [`Resources`]. + #[cfg(all(feature = "item_interactions", feature = "item_state_example"))] + fn interactions_example( + &self, + params_specs: &ParamsSpecs, + resources: &Resources, + ) -> Result; + + /// Returns the physical resources that this item interacts with, merging + /// any available current state over example state. + /// + /// # Design + /// + /// This method returns interactions from [`Item::interactions`], passing in + /// parameters computed from current state, or if not available, example + /// state. + /// + /// For tracking which item interactions are known, for the purpose of + /// styling unknown state differently, we could return the + /// `ItemInteractions` alongside with how they were constructed: + /// + /// 1. One for `ItemInteraction`s where params are fully computed using + /// fully known state. + /// 2. One for `ItemInteraction`s where params are computed using some or + /// all example state. + /// + /// ## Fallibility + /// + /// [`Item::interactions`] is infallible as computing `ItemInteractions` + /// should purely be instantiating objects. + /// + /// [`ItemRt::interactions_current`] *is* fallible as value resolution + /// for parameters may fail, e.g. if there is a bug in Peace, or an + /// item's parameters requests a type that doesn't exist in + /// [`Resources`]. + #[cfg(all(feature = "item_interactions", feature = "item_state_example"))] + fn interactions_try_current( + &self, + params_specs: &ParamsSpecs, + resources: &Resources, + ) -> Result; + + /// Returns a human readable tag name that represents this item. + /// + /// For example, a `FileDownloadItem` should return a string similar + /// to: `"Web App: File Download"`. This allows tags to be grouped by the + /// concept / information they are associated with, rather than grouping + /// tags by the type of operation. + #[cfg(all(feature = "item_interactions", feature = "item_state_example"))] + fn interactions_tag_name(&self) -> String; } diff --git a/crate/rt_model/src/item_wrapper.rs b/crate/rt_model/src/item_wrapper.rs index 915ff249b..3cd7ea3c1 100644 --- a/crate/rt_model/src/item_wrapper.rs +++ b/crate/rt_model/src/item_wrapper.rs @@ -11,7 +11,9 @@ use peace_data::{ marker::{ApplyDry, Clean, Current, Goal}, Data, }; -use peace_params::{Params, ParamsSpec, ParamsSpecs, ValueResolutionCtx, ValueResolutionMode}; +use peace_params::{ + Params, ParamsMergeExt, ParamsSpec, ParamsSpecs, ValueResolutionCtx, ValueResolutionMode, +}; use peace_resource_rt::{ resources::ts::{Empty, SetUp}, states::StatesCurrent, @@ -25,6 +27,13 @@ use crate::{ ItemRt, ParamsSpecsTypeReg, StateDowncastError, StatesTypeReg, }; +#[cfg(feature = "output_progress")] +use peace_cfg::RefInto; +#[cfg(feature = "item_state_example")] +use peace_data::marker::Example; +#[cfg(feature = "output_progress")] +use peace_item_model::ItemLocationState; + /// Wraps a type implementing [`Item`]. /// /// # Type Parameters @@ -69,28 +78,30 @@ where TryFrom<<::Params<'params> as Params>::Partial>, for<'params> as Params>::Partial: From>, { + #[cfg(feature = "item_state_example")] + fn state_example( + &self, + params_specs: &ParamsSpecs, + resources: &Resources, + ) -> Result { + let state_example = { + let params = self.params(params_specs, resources, ValueResolutionMode::Example)?; + let data = as Data>::borrow(self.id(), resources); + I::state_example(¶ms, data) + }; + resources.borrow_mut::>().0 = Some(state_example.clone()); + + Ok(state_example) + } + async fn state_clean( &self, params_specs: &ParamsSpecs, resources: &Resources, ) -> Result { let state_clean = { - let params_partial = { - let item_id = self.id(); - let params_spec = params_specs - .get::>, _>(item_id) - .ok_or_else(|| crate::Error::ParamsSpecNotFound { - item_id: item_id.clone(), - })?; - let mut value_resolution_ctx = ValueResolutionCtx::new( - ValueResolutionMode::Clean, - item_id.clone(), - tynm::type_name::>(), - ); - params_spec - .resolve_partial(resources, &mut value_resolution_ctx) - .map_err(crate::Error::ParamsResolveError)? - }; + let params_partial = + self.params_partial(params_specs, resources, ValueResolutionMode::Clean)?; let data = as Data>::borrow(self.id(), resources); I::state_clean(¶ms_partial, data).await? }; @@ -106,27 +117,18 @@ where fn_ctx: FnCtx<'_>, ) -> Result, E> { let state_current = { - let params_partial = { - let item_id = self.id(); - let params_spec = params_specs - .get::>, _>(item_id) - .ok_or_else(|| crate::Error::ParamsSpecNotFound { - item_id: item_id.clone(), - })?; - let mut value_resolution_ctx = ValueResolutionCtx::new( - ValueResolutionMode::Current, - item_id.clone(), - tynm::type_name::>(), - ); - params_spec - .resolve_partial(resources, &mut value_resolution_ctx) - .map_err(crate::Error::ParamsResolveError)? - }; + let params_partial = + self.params_partial(params_specs, resources, ValueResolutionMode::Current)?; let data = as Data>::borrow(self.id(), resources); I::try_state_current(fn_ctx, ¶ms_partial, data).await? }; if let Some(state_current) = state_current.as_ref() { resources.borrow_mut::>().0 = Some(state_current.clone()); + + #[cfg(feature = "output_progress")] + fn_ctx + .progress_sender() + .item_location_state_send(RefInto::::into(state_current)); } Ok(state_current) @@ -139,27 +141,17 @@ where fn_ctx: FnCtx<'_>, ) -> Result { let state_current = { - let params = { - let item_id = self.id(); - let params_spec = params_specs - .get::>, _>(item_id) - .ok_or_else(|| crate::Error::ParamsSpecNotFound { - item_id: item_id.clone(), - })?; - let mut value_resolution_ctx = ValueResolutionCtx::new( - ValueResolutionMode::Current, - item_id.clone(), - tynm::type_name::>(), - ); - params_spec - .resolve(resources, &mut value_resolution_ctx) - .map_err(crate::Error::ParamsResolveError)? - }; + let params = self.params(params_specs, resources, ValueResolutionMode::Current)?; let data = as Data>::borrow(self.id(), resources); I::state_current(fn_ctx, ¶ms, data).await? }; resources.borrow_mut::>().0 = Some(state_current.clone()); + #[cfg(feature = "output_progress")] + fn_ctx + .progress_sender() + .item_location_state_send(RefInto::::into(&state_current)); + Ok(state_current) } @@ -169,22 +161,15 @@ where resources: &Resources, fn_ctx: FnCtx<'_>, ) -> Result, E> { - let params_partial = { - let item_id = self.id(); - let params_spec = params_specs - .get::>, _>(item_id) - .ok_or_else(|| crate::Error::ParamsSpecNotFound { - item_id: item_id.clone(), - })?; - let mut value_resolution_ctx = ValueResolutionCtx::new( - ValueResolutionMode::Goal, - item_id.clone(), - tynm::type_name::>(), - ); - params_spec - .resolve_partial(resources, &mut value_resolution_ctx) - .map_err(crate::Error::ParamsResolveError)? - }; + let params_partial = + self.params_partial(params_specs, resources, ValueResolutionMode::Goal)?; + + // If a predecessor's goal state is the same as current, then a successor's + // `state_goal_try_exec` should kind of use `ValueResolutionMode::Current`. + // + // But really we should insert the predecessor's current state as the + // `Goal`. + let data = as Data>::borrow(self.id(), resources); let state_goal = I::try_state_goal(fn_ctx, ¶ms_partial, data).await?; if let Some(state_goal) = state_goal.as_ref() { @@ -216,22 +201,7 @@ where resources: &Resources, fn_ctx: FnCtx<'_>, ) -> Result { - let params = { - let item_id = self.id(); - let params_spec = params_specs - .get::>, _>(item_id) - .ok_or_else(|| crate::Error::ParamsSpecNotFound { - item_id: item_id.clone(), - })?; - let mut value_resolution_ctx = ValueResolutionCtx::new( - value_resolution_mode, - item_id.clone(), - tynm::type_name::>(), - ); - params_spec - .resolve(resources, &mut value_resolution_ctx) - .map_err(crate::Error::ParamsResolveError)? - }; + let params = self.params(params_specs, resources, value_resolution_mode)?; let data = as Data>::borrow(self.id(), resources); let state_goal = I::state_goal(fn_ctx, ¶ms, data).await?; resources.borrow_mut::>().0 = Some(state_goal.clone()); @@ -276,36 +246,21 @@ where state_b: &I::State, ) -> Result { let state_diff: I::StateDiff = { - let params_partial = { - let item_id = self.id(); - let params_spec = params_specs - .get::>, _>(item_id) - .ok_or_else(|| crate::Error::ParamsSpecNotFound { - item_id: item_id.clone(), - })?; - - // Running `diff` for a single profile will be between the current and goal - // states, and parameters are not really intended to be used for diffing. - // - // However for `ShCmdItem`, the shell script for diffing's path is in - // params, which *likely* would be provided as direct `Value`s instead of - // mapped from predecessors' state(s). Iff the values are mapped from a - // predecessor's state, then we would want it to be the goal state, as that - // is closest to the correct value -- `ValueResolutionMode::ApplyDry` is used in - // `Item::apply_dry`, and `ValueResolutionMode::Apply` is used in - // `Item::apply`. - // - // Running `diff` for multiple profiles will likely be between two profiles' - // current states. - let mut value_resolution_ctx = ValueResolutionCtx::new( - ValueResolutionMode::Goal, - item_id.clone(), - tynm::type_name::>(), - ); - params_spec - .resolve_partial(resources, &mut value_resolution_ctx) - .map_err(crate::Error::ParamsResolveError)? - }; + // Running `diff` for a single profile will be between the current and goal + // states, and parameters are not really intended to be used for diffing. + // + // However for `ShCmdItem`, the shell script for diffing's path is in + // params, which *likely* would be provided as direct `Value`s instead of + // mapped from predecessors' state(s). Iff the values are mapped from a + // predecessor's state, then we would want it to be the goal state, as that + // is closest to the correct value -- `ValueResolutionMode::ApplyDry` is used in + // `Item::apply_dry`, and `ValueResolutionMode::Apply` is used in + // `Item::apply`. + // + // Running `diff` for multiple profiles will likely be between two profiles' + // current states. + let params_partial = + self.params_partial(params_specs, resources, ValueResolutionMode::Goal)?; let data = as Data>::borrow(self.id(), resources); I::state_diff(¶ms_partial, data, state_a, state_b) .await @@ -324,30 +279,15 @@ where state_diff: &I::StateDiff, value_resolution_mode: ValueResolutionMode, ) -> Result { - let params_partial = { - let item_id = self.id(); - let params_spec = params_specs - .get::>, _>(item_id) - .ok_or_else(|| crate::Error::ParamsSpecNotFound { - item_id: item_id.clone(), - })?; - - // Normally an `apply_check` only compares the states / state diff. - // - // We use `ValueResolutionMode::Goal` because an apply is between the current - // and goal states, and when resolving values, we want the target state's - // parameters to be used. Note that during an apply, the goal state is - // resolved as execution happens -- values that rely on predecessors' applied - // state will be fed into successors' goal state. - let mut value_resolution_ctx = ValueResolutionCtx::new( - value_resolution_mode, - item_id.clone(), - tynm::type_name::>(), - ); - params_spec - .resolve_partial(resources, &mut value_resolution_ctx) - .map_err(crate::Error::ParamsResolveError)? - }; + // Normally an `apply_check` only compares the states / state diff. + // + // We use `ValueResolutionMode::Goal` because an apply is between the current + // and goal states, and when resolving values, we want the target state's + // parameters to be used. Note that during an apply, the goal state is + // resolved as execution happens -- values that rely on predecessors' applied + // state will be fed into successors' goal state. + let params_partial = self.params_partial(params_specs, resources, value_resolution_mode)?; + let data = as Data>::borrow(self.id(), resources); if let Ok(params) = params_partial.try_into() { I::apply_check(¶ms, data, state_current, state_target, state_diff) @@ -372,22 +312,7 @@ where state_goal: &I::State, state_diff: &I::StateDiff, ) -> Result { - let params = { - let item_id = self.id(); - let params_spec = params_specs - .get::>, _>(item_id) - .ok_or_else(|| crate::Error::ParamsSpecNotFound { - item_id: item_id.clone(), - })?; - let mut value_resolution_ctx = ValueResolutionCtx::new( - ValueResolutionMode::ApplyDry, - item_id.clone(), - tynm::type_name::>(), - ); - params_spec - .resolve(resources, &mut value_resolution_ctx) - .map_err(crate::Error::ParamsResolveError)? - }; + let params = self.params(params_specs, resources, ValueResolutionMode::ApplyDry)?; let data = as Data>::borrow(self.id(), resources); let state_ensured_dry = I::apply_dry(fn_ctx, ¶ms, data, state_current, state_goal, state_diff) @@ -408,22 +333,7 @@ where state_goal: &I::State, state_diff: &I::StateDiff, ) -> Result { - let params = { - let item_id = self.id(); - let params_spec = params_specs - .get::>, _>(item_id) - .ok_or_else(|| crate::Error::ParamsSpecNotFound { - item_id: item_id.clone(), - })?; - let mut value_resolution_ctx = ValueResolutionCtx::new( - ValueResolutionMode::Current, - item_id.clone(), - tynm::type_name::>(), - ); - params_spec - .resolve(resources, &mut value_resolution_ctx) - .map_err(crate::Error::ParamsResolveError)? - }; + let params = self.params(params_specs, resources, ValueResolutionMode::Current)?; let data = as Data>::borrow(self.id(), resources); let state_ensured = I::apply(fn_ctx, ¶ms, data, state_current, state_goal, state_diff) .await @@ -431,8 +341,57 @@ where resources.borrow_mut::>().0 = Some(state_ensured.clone()); + #[cfg(feature = "output_progress")] + fn_ctx + .progress_sender() + .item_location_state_send(RefInto::::into(&state_ensured)); + Ok(state_ensured) } + + fn params_partial( + &self, + params_specs: &ParamsSpecs, + resources: &Resources, + value_resolution_mode: ValueResolutionMode, + ) -> Result<<::Params<'_> as Params>::Partial, E> { + let item_id = self.id(); + let params_spec = params_specs + .get::>, _>(item_id) + .ok_or_else(|| crate::Error::ParamsSpecNotFound { + item_id: item_id.clone(), + })?; + let mut value_resolution_ctx = ValueResolutionCtx::new( + value_resolution_mode, + item_id.clone(), + tynm::type_name::>(), + ); + Ok(params_spec + .resolve_partial(resources, &mut value_resolution_ctx) + .map_err(crate::Error::ParamsResolveError)?) + } + + fn params( + &self, + params_specs: &ParamsSpecs, + resources: &Resources, + value_resolution_mode: ValueResolutionMode, + ) -> Result<::Params<'_>, E> { + let item_id = self.id(); + let params_spec = params_specs + .get::>, _>(item_id) + .ok_or_else(|| crate::Error::ParamsSpecNotFound { + item_id: item_id.clone(), + })?; + let mut value_resolution_ctx = ValueResolutionCtx::new( + value_resolution_mode, + item_id.clone(), + tynm::type_name::>(), + ); + Ok(params_spec + .resolve(resources, &mut value_resolution_ctx) + .map_err(crate::Error::ParamsResolveError)?) + } } impl Debug for ItemWrapper @@ -513,9 +472,10 @@ where + From<::Error> + From + 'static, - for<'params> ::Params<'params>: - TryFrom<<::Params<'params> as Params>::Partial>, - for<'params> as Params>::Partial: From>, + for<'params> I::Params<'params>: + ParamsMergeExt + TryFrom< as Params>::Partial>, + for<'params> as Params>::Partial: From> + + From< as TryFrom< as Params>::Partial>>::Error>, { fn id(&self) -> &ItemId { ::id(self) @@ -544,6 +504,8 @@ where async fn setup(&self, resources: &mut Resources) -> Result<(), E> { // Insert `XMarker` to create entries in `Resources`. // This is used for referential param values (#94) + #[cfg(feature = "item_state_example")] + resources.insert(Example::(None)); resources.insert(Clean::(None)); resources.insert(Current::(None)); resources.insert(Goal::(None)); @@ -591,6 +553,16 @@ where } } + #[cfg(feature = "item_state_example")] + fn state_example( + &self, + params_specs: &ParamsSpecs, + resources: &Resources, + ) -> Result { + self.state_example(params_specs, resources) + .map(BoxDtDisplay::new) + } + async fn state_clean( &self, params_specs: &ParamsSpecs, @@ -979,4 +951,89 @@ where Ok(()) } + + #[cfg(all(feature = "item_interactions", feature = "item_state_example"))] + fn interactions_example( + &self, + params_specs: &ParamsSpecs, + resources: &Resources, + ) -> Result { + let params = self.params(params_specs, resources, ValueResolutionMode::Example)?; + + let data = as Data>::borrow(self.id(), resources); + + let item_interactions = I::interactions(¶ms, data); + + Ok(peace_item_model::ItemInteractionsExample::from( + item_interactions, + )) + } + + #[cfg(all(feature = "item_interactions", feature = "item_state_example"))] + fn interactions_try_current<'params>( + &self, + params_specs: &ParamsSpecs, + resources: &Resources, + ) -> Result { + let params_partial_current = + self.params_partial(params_specs, resources, ValueResolutionMode::Current)?; + let mut params_example = + self.params(params_specs, resources, ValueResolutionMode::Example)?; + let params_current_result: Result, _> = + TryFrom::<_>::try_from(params_partial_current); + + let data = as Data>::borrow(self.id(), resources); + let item_interactions = match params_current_result { + Ok(params_current) => { + let item_interactions = I::interactions(¶ms_current, data); + + peace_item_model::ItemInteractionsCurrent::from(item_interactions).into() + } + Err(params_partial_current) => { + // Rust cannot guarantee that `I::Params.try_from(params_partial)`'s + // `TryFrom::Error` type is exactly the same as `Params::Partial`, so we have to + // explicitly add the `ParamsPartial: From` bound, and call + // `.into()` over here. + ParamsMergeExt::merge(&mut params_example, params_partial_current.into()); + let params_merged = params_example; + let item_interactions = I::interactions(¶ms_merged, data); + + peace_item_model::ItemInteractionsExample::from(item_interactions).into() + } + }; + + Ok(item_interactions) + } + + #[cfg(all(feature = "item_interactions", feature = "item_state_example"))] + fn interactions_tag_name(&self) -> String { + use std::borrow::Cow; + + let type_name = tynm::type_name::(); + let (operation, prefix) = type_name + .split_once("<") + .map(|(operation, prefix_plus_extra)| { + let prefix_end = prefix_plus_extra.find(['<', '>']); + let prefix = prefix_end + .map(|prefix_end| &prefix_plus_extra[..prefix_end]) + .unwrap_or(prefix_plus_extra); + (Cow::Borrowed(operation), Some(Cow::Borrowed(prefix))) + }) + .unwrap_or_else(|| (Cow::Borrowed(&type_name), None)); + + // Subtract `Item` suffix + let operation = match operation.rsplit_once("Item") { + Some((operation_minus_item, _)) => Cow::Borrowed(operation_minus_item), + None => operation, + }; + + match prefix { + Some(prefix) => { + let prefix = heck::AsTitleCase(prefix).to_string(); + let operation = heck::AsTitleCase(operation).to_string(); + format!("{prefix}: {operation}") + } + None => heck::AsTitleCase(operation).to_string(), + } + } } diff --git a/crate/rt_model/src/outcomes/item_apply_partial_rt.rs b/crate/rt_model/src/outcomes/item_apply_partial_rt.rs index f0cf0ab73..af50f0e37 100644 --- a/crate/rt_model/src/outcomes/item_apply_partial_rt.rs +++ b/crate/rt_model/src/outcomes/item_apply_partial_rt.rs @@ -57,7 +57,7 @@ impl ItemApplyPartialRt for Box { } } -impl<'a> serde::Serialize for dyn ItemApplyPartialRt + 'a { +impl serde::Serialize for dyn ItemApplyPartialRt + '_ { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, diff --git a/crate/rt_model/src/outcomes/item_apply_rt.rs b/crate/rt_model/src/outcomes/item_apply_rt.rs index eb5a6eb64..b400edab9 100644 --- a/crate/rt_model/src/outcomes/item_apply_rt.rs +++ b/crate/rt_model/src/outcomes/item_apply_rt.rs @@ -64,7 +64,7 @@ impl ItemApplyRt for Box { } } -impl<'a> serde::Serialize for dyn ItemApplyRt + 'a { +impl serde::Serialize for dyn ItemApplyRt + '_ { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, diff --git a/crate/rt_model_core/Cargo.toml b/crate/rt_model_core/Cargo.toml index 04ee355d4..a24d21567 100644 --- a/crate/rt_model_core/Cargo.toml +++ b/crate/rt_model_core/Cargo.toml @@ -28,6 +28,7 @@ miette = { workspace = true, optional = true } peace_core = { workspace = true } peace_cmd_model = { workspace = true } peace_fmt = { workspace = true } +peace_item_model = { workspace = true, optional = true } peace_params = { workspace = true } peace_resource_rt = { workspace = true } serde = { workspace = true } @@ -43,4 +44,8 @@ base64 = { workspace = true } default = [] error_reporting = ["dep:miette", "peace_cmd_model/error_reporting"] output_in_memory = ["indicatif/in_memory"] -output_progress = ["peace_core/output_progress"] +output_progress = [ + "dep:peace_item_model", + "peace_core/output_progress", + "peace_item_model/output_progress", +] diff --git a/crate/rt_model_core/src/error.rs b/crate/rt_model_core/src/error.rs index 0fa22df34..e40be05ef 100644 --- a/crate/rt_model_core/src/error.rs +++ b/crate/rt_model_core/src/error.rs @@ -145,7 +145,7 @@ pub enum Error { help("{}", params_specs_mismatch_display( item_ids_with_no_params_specs, params_specs_provided_mismatches, - params_specs_stored_mismatches.as_ref(), + params_specs_stored_mismatches.as_ref().as_ref(), params_specs_not_usable, )) ) @@ -156,7 +156,10 @@ pub enum Error { /// Provided params specs with no matching item ID in the flow. params_specs_provided_mismatches: ParamsSpecs, /// Stored params specs with no matching item ID in the flow. - params_specs_stored_mismatches: Option, + // + // Boxed so that this enum variant is not so large compared to other variants + // to address `clippy::large_enum_variant`. + params_specs_stored_mismatches: Box>, /// Item IDs which had a mapping function previously provided in /// its params spec, but on a subsequent invocation nothing was /// provided. diff --git a/crate/rt_model_core/src/output/output_write.rs b/crate/rt_model_core/src/output/output_write.rs index 33c1100ca..84414e26c 100644 --- a/crate/rt_model_core/src/output/output_write.rs +++ b/crate/rt_model_core/src/output/output_write.rs @@ -5,7 +5,11 @@ use peace_fmt::Presentable; cfg_if::cfg_if! { if #[cfg(feature = "output_progress")] { - use peace_core::progress::{ProgressTracker, ProgressUpdateAndId}; + use peace_core::progress::{ + CmdBlockItemInteractionType, + ProgressTracker, + ProgressUpdateAndId, + }; use crate::CmdProgressTracker; } @@ -39,6 +43,34 @@ pub trait OutputWrite: Debug + Unpin { #[cfg(feature = "output_progress")] async fn progress_begin(&mut self, cmd_progress_tracker: &CmdProgressTracker); + /// Indicates a particular `CmdBlock` has begun. + /// + /// # Implementors + /// + /// This is called whenever a different `CmdBlock` is started. + #[cfg(feature = "output_progress")] + async fn cmd_block_start( + &mut self, + cmd_block_item_interaction_type: CmdBlockItemInteractionType, + ); + + /// Signals an update of an `Item`'s `ItemLocationState`. + /// + /// # Implementors + /// + /// This is called when an `Item`'s current `State` is updated. + /// + /// # Maintainers + /// + /// The `ItemLocationState` is first constructed in `ItemWrapper`, and this + /// method is invoked in `Progress`. + #[cfg(feature = "output_progress")] + async fn item_location_state( + &mut self, + item_id: peace_core::ItemId, + item_location_state: peace_item_model::ItemLocationState, + ); + /// Renders progress information, and returns when no more progress /// information is available to write. /// diff --git a/crate/webi/Cargo.toml b/crate/webi/Cargo.toml index 0fd9473e0..a2458dc2e 100644 --- a/crate/webi/Cargo.toml +++ b/crate/webi/Cargo.toml @@ -26,7 +26,16 @@ peace_webi_output = { workspace = true, optional = true } [features] default = [] +item_interactions = [ + "peace_webi_components/item_interactions", + "peace_webi_output?/item_interactions", +] +item_state_example = [ + "peace_webi_components/item_state_example", + "peace_webi_output?/item_state_example", +] output_progress = [ + "peace_webi_model/output_progress", "peace_webi_output?/output_progress", ] ssr = [ diff --git a/crate/webi_components/Cargo.toml b/crate/webi_components/Cargo.toml index cdbcb53a0..c68623878 100644 --- a/crate/webi_components/Cargo.toml +++ b/crate/webi_components/Cargo.toml @@ -20,14 +20,39 @@ doctest = true test = false [dependencies] -dot_ix = { workspace = true, features = ["rt", "web_components", "flex_diag"] } +dot_ix = { workspace = true, features = ["rt", "web_components"] } +futures = { workspace = true } +gloo-timers = { workspace = true, features = ["futures"] } leptos = { workspace = true } leptos_meta = { workspace = true } leptos_router = { workspace = true } +peace_cmd = { workspace = true } +peace_cmd_model = { workspace = true } +peace_core = { workspace = true } peace_flow_model = { workspace = true } +peace_item_model = { workspace = true } +peace_params = { workspace = true } +peace_resource_rt = { workspace = true } +peace_rt_model = { workspace = true } +peace_webi_model = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true, features = ["sync"] } + +# Used to print serialized info graph for debugging. +serde_yaml = { workspace = true } [features] default = [] + +# Technically always needed, but we need to put these in its own feature so that +# these aren't enabled in the underlying crates when `--no-default-features` is +# used. +item_interactions = ["peace_rt_model/item_interactions"] +item_state_example = [ + "peace_cmd/item_state_example", + "peace_rt_model/item_state_example", +] + ssr = [ "dot_ix/ssr", "leptos/ssr", diff --git a/crate/webi_components/src/children_fn.rs b/crate/webi_components/src/children_fn.rs new file mode 100644 index 000000000..019e33887 --- /dev/null +++ b/crate/webi_components/src/children_fn.rs @@ -0,0 +1,57 @@ +use std::{fmt, sync::Arc}; + +use leptos::{Fragment, IntoView, ToChildren}; + +/// Allows a consumer to pass in the view fragment for a +/// [`leptos_router::Route`]. +/// +/// # Design +/// +/// In `leptos 0.6`, `leptos::ChildrenFn` is an alias for `Rc<_>`, so it cannot +/// be passed to `leptos_axum::Router::leptos_routes`'s `app_fn` which requires +/// `app_fn` to be `Clone`, so we need to create our own `ChildrenFn` which is +/// `Clone`. +/// +/// When we migrate to `leptos 0.7`, `ChildrenFn` is an alias for `Arc<_>` so we +/// can use it directly. +#[derive(Clone)] +pub struct ChildrenFn(Arc Fragment + Send + Sync>); + +impl ChildrenFn { + /// Returns a new `ChildrenFn`; + pub fn new(f: F) -> Self + where + F: Fn() -> IV + Send + Sync + 'static, + IV: IntoView, + { + Self(Arc::new(move || Fragment::from(f().into_view()))) + } + + /// Returns the underlying function. + pub fn into_inner(self) -> Arc Fragment + Send + Sync> { + self.0 + } + + /// Calls the inner function to render the view. + pub fn call(&self) -> Fragment { + (self.0)() + } +} + +impl fmt::Debug for ChildrenFn { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("ChildrenFn") + .field(&"Arc Fragment + Send + Sync>") + .finish() + } +} + +impl ToChildren for ChildrenFn +where + F: Fn() -> Fragment + 'static + Send + Sync, +{ + #[inline] + fn to_children(f: F) -> Self { + ChildrenFn(Arc::new(f)) + } +} diff --git a/crate/webi_components/src/flow_graph.rs b/crate/webi_components/src/flow_graph.rs index 1809031b3..d94df167c 100644 --- a/crate/webi_components/src/flow_graph.rs +++ b/crate/webi_components/src/flow_graph.rs @@ -1,81 +1,140 @@ use dot_ix::{ - model::{common::DotSrcAndStyles, info_graph::InfoGraph}, - web_components::{DotSvg, FlexDiag}, -}; -use leptos::{ - component, server_fn::error::NoCustomError, view, IntoView, ServerFnError, Signal, SignalGet, - Transition, + model::{common::GraphvizDotTheme, info_graph::InfoGraph}, + rt::IntoGraphvizDotSrc, + web_components::DotSvg, }; +use leptos::{component, server, view, IntoView, ServerFnError, SignalSet, Transition}; /// Renders the flow graph. +/// +/// # Future +/// +/// * Take in whether any execution is running. Use that info to style +/// nodes/edges. +/// * Take in values so they can be rendered, or `WriteSignal`s, to notify the +/// component that will render values about which node is selected. #[component] pub fn FlowGraph() -> impl IntoView { - let progress_dot_resource = leptos::create_resource( - || (), - move |()| async move { progress_dot_graph().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."); + view! { +
+ + +
+ } +} - Some(progress_dot_graph) - }; +#[server] +async fn progress_info_graph_fetch() -> Result { + use leptos::{ReadSignal, SignalGet}; + use peace_core::FlowId; + use peace_webi_model::FlowProgressInfoGraphs; - let outcome_info_graph_resource = leptos::create_resource( - || (), - move |()| async move { outcome_info_graph().await.unwrap() }, - ); - let outcome_info_graph = move || { - let outcome_info_graph = - Signal::from(move || outcome_info_graph_resource.get().unwrap_or_default()); + let flow_id = leptos::use_context::>(); + let flow_progress_info_graphs = leptos::use_context::>(); + let progress_info_graph = if let Some(flow_progress_info_graphs) = flow_progress_info_graphs { + let flow_progress_info_graphs = flow_progress_info_graphs.lock().ok(); - view! { - - } + flow_id + .as_ref() + .map(SignalGet::get) + .zip(flow_progress_info_graphs) + .and_then(|(flow_id, flow_progress_info_graphs)| { + flow_progress_info_graphs.get(&flow_id).cloned() + }) + .unwrap_or_default() + } else { + InfoGraph::default() }; + Ok(progress_info_graph) +} + +#[component] +fn ProgressGraph() -> impl IntoView { + let (progress_info_graph, progress_info_graph_set) = + leptos::create_signal(InfoGraph::default()); + let (dot_src_and_styles, dot_src_and_styles_set) = leptos::create_signal(None); + + leptos::create_local_resource( + move || (), + move |()| async move { + let progress_info_graph = progress_info_graph_fetch().await.unwrap_or_default(); + let dot_src_and_styles = + IntoGraphvizDotSrc::into(&progress_info_graph, &GraphvizDotTheme::default()); + + if let Ok(progress_info_graph_serialized) = serde_yaml::to_string(&progress_info_graph) + { + leptos::logging::log!("{progress_info_graph_serialized}"); + } + + progress_info_graph_set.set(progress_info_graph); + dot_src_and_styles_set.set(Some(dot_src_and_styles)); + }, + ); + view! { "Loading graph..."

}> -
- - {outcome_info_graph} -
+
} } -/// Returns the graph representing item execution progress. -#[leptos::server(endpoint = "/flow_graph")] -pub async fn progress_dot_graph() -> Result> { - use dot_ix::{ - model::common::{graphviz_dot_theme::GraphStyle, GraphvizDotTheme}, - rt::IntoGraphvizDotSrc, - }; - use peace_flow_model::FlowSpecInfo; +#[server] +async fn outcome_info_graph_fetch() -> Result { + use leptos::{ReadSignal, SignalGet}; + use peace_core::FlowId; + use peace_webi_model::FlowOutcomeInfoGraphs; - let flow_spec_info = leptos::use_context::().ok_or_else(|| { - ServerFnError::::ServerError("`FlowSpecInfo` was not set.".to_string()) - })?; + let flow_id = leptos::use_context::>(); + let flow_outcome_info_graphs = leptos::use_context::>(); + let outcome_info_graph = if let Some(flow_outcome_info_graphs) = flow_outcome_info_graphs { + let flow_outcome_info_graphs = flow_outcome_info_graphs.lock().ok(); + + flow_id + .as_ref() + .map(SignalGet::get) + .zip(flow_outcome_info_graphs) + .and_then(|(flow_id, flow_outcome_info_graphs)| { + flow_outcome_info_graphs.get(&flow_id).cloned() + }) + .unwrap_or_default() + } else { + InfoGraph::default() + }; - let progress_info_graph = flow_spec_info.to_progress_info_graph(); - Ok(IntoGraphvizDotSrc::into( - &progress_info_graph, - &GraphvizDotTheme::default().with_graph_style(GraphStyle::Circle), - )) + Ok(outcome_info_graph) } -/// Returns the graph representing item outcomes. -#[leptos::server(endpoint = "/flow_graph")] -pub async fn outcome_info_graph() -> Result> { - use peace_flow_model::FlowSpecInfo; +#[component] +fn OutcomeGraph() -> impl IntoView { + let (outcome_info_graph, outcome_info_graph_set) = leptos::create_signal(InfoGraph::default()); + let (dot_src_and_styles, dot_src_and_styles_set) = leptos::create_signal(None); - let flow_spec_info = leptos::use_context::().ok_or_else(|| { - ServerFnError::::ServerError("`FlowSpecInfo` was not set.".to_string()) - })?; + leptos::create_local_resource( + move || (), + move |()| async move { + let outcome_info_graph = outcome_info_graph_fetch().await.unwrap_or_default(); + let dot_src_and_styles = + IntoGraphvizDotSrc::into(&outcome_info_graph, &GraphvizDotTheme::default()); - let outcome_info_graph = flow_spec_info.to_outcome_info_graph(); - Ok(outcome_info_graph) + if let Ok(outcome_info_graph_serialized) = serde_yaml::to_string(&outcome_info_graph) { + leptos::logging::log!("{outcome_info_graph_serialized}"); + } + + outcome_info_graph_set.set(outcome_info_graph); + dot_src_and_styles_set.set(Some(dot_src_and_styles)); + }, + ); + + view! { + "Loading graph..."

}> + +
+ } } diff --git a/crate/webi_components/src/flow_graph_current.rs b/crate/webi_components/src/flow_graph_current.rs new file mode 100644 index 000000000..92bbff407 --- /dev/null +++ b/crate/webi_components/src/flow_graph_current.rs @@ -0,0 +1,122 @@ +use dot_ix::{ + model::{common::GraphvizDotTheme, info_graph::InfoGraph}, + rt::IntoGraphvizDotSrc, + web_components::DotSvg, +}; +use leptos::{ + component, server, view, IntoView, ServerFnError, SignalGetUntracked, SignalSet, Transition, +}; + +/// Renders the flow graph. +/// +/// # Future +/// +/// * Take in whether any execution is running. Use that info to style +/// nodes/edges. +/// * Take in values so they can be rendered, or `WriteSignal`s, to notify the +/// component that will render values about which node is selected. +#[component] +pub fn FlowGraphCurrent() -> impl IntoView { + let (progress_info_graph_get, progress_info_graph_set) = + leptos::create_signal(InfoGraph::default()); + let (progress_dot_src_and_styles, progress_dot_src_and_styles_set) = + leptos::create_signal(None); + + let (outcome_info_graph_get, outcome_info_graph_set) = + leptos::create_signal(InfoGraph::default()); + let (outcome_dot_src_and_styles, outcome_dot_src_and_styles_set) = leptos::create_signal(None); + + leptos::create_local_resource( + move || (), + move |()| async move { + use gloo_timers::future::TimeoutFuture; + + loop { + if let Ok(Some((progress_info_graph, outcome_info_graph))) = + info_graphs_fetch().await + { + // Progress + let progress_dot_src_and_styles = IntoGraphvizDotSrc::into( + &progress_info_graph, + &GraphvizDotTheme::default(), + ); + + if progress_info_graph != progress_info_graph_get.get_untracked() { + progress_info_graph_set.set(progress_info_graph); + progress_dot_src_and_styles_set.set(Some(progress_dot_src_and_styles)); + } + + // Outcome + let outcome_dot_src_and_styles = + IntoGraphvizDotSrc::into(&outcome_info_graph, &GraphvizDotTheme::default()); + + if outcome_info_graph != outcome_info_graph_get.get_untracked() { + if let Ok(outcome_info_graph_serialized) = + serde_yaml::to_string(&outcome_info_graph) + { + leptos::logging::log!("{outcome_info_graph_serialized}"); + } + + outcome_info_graph_set.set(outcome_info_graph); + outcome_dot_src_and_styles_set.set(Some(outcome_dot_src_and_styles)); + } + } + + TimeoutFuture::new(250).await; + } + }, + ); + + view! { +
+ "Loading graph..."

}> + + +
+
+ } +} + +#[server] +async fn info_graphs_fetch() -> Result, ServerFnError> { + use std::sync::{Arc, Mutex}; + + use peace_cmd_model::CmdExecutionId; + use peace_webi_model::{FlowOutcomeInfoGraphs, FlowProgressInfoGraphs}; + + let cmd_execution_id = leptos::use_context::>>>(); + let flow_progress_info_graphs = leptos::use_context::>(); + let flow_outcome_info_graphs = leptos::use_context::>(); + + if let Some(((cmd_execution_id, flow_progress_info_graphs), flow_outcome_info_graphs)) = + cmd_execution_id + .zip(flow_progress_info_graphs) + .zip(flow_outcome_info_graphs) + { + let cmd_execution_id = cmd_execution_id.lock().ok().as_deref().copied().flatten(); + let flow_progress_info_graphs = flow_progress_info_graphs.lock().ok(); + + let progress_info_graph = cmd_execution_id.zip(flow_progress_info_graphs).and_then( + |(cmd_execution_id, flow_progress_info_graphs)| { + flow_progress_info_graphs.get(&cmd_execution_id).cloned() + }, + ); + + let flow_outcome_info_graphs = flow_outcome_info_graphs.lock().ok(); + let outcome_info_graph = cmd_execution_id.zip(flow_outcome_info_graphs).and_then( + |(cmd_execution_id, flow_outcome_info_graphs)| { + flow_outcome_info_graphs.get(&cmd_execution_id).cloned() + }, + ); + + Ok(progress_info_graph.zip(outcome_info_graph)) + } else { + Ok(None) + } +} diff --git a/crate/webi_components/src/home.rs b/crate/webi_components/src/home.rs index df5e37682..c54b255f9 100644 --- a/crate/webi_components/src/home.rs +++ b/crate/webi_components/src/home.rs @@ -2,11 +2,15 @@ use leptos::{component, view, IntoView}; use leptos_meta::{provide_meta_context, Link, Stylesheet}; use leptos_router::{Route, Router, Routes}; -use crate::FlowGraph; +use crate::ChildrenFn; /// Top level component of the `WebiOutput`. +/// +/// # Parameters: +/// +/// * `flow_component`: The web component to render for the flow. #[component] -pub fn Home() -> impl IntoView { +pub fn Home(app_home: ChildrenFn) -> impl IntoView { // Provides context that manages stylesheets, titles, meta tags, etc. provide_meta_context(); @@ -20,9 +24,10 @@ pub fn Home() -> impl IntoView {
- - }/> +
diff --git a/crate/webi_components/src/lib.rs b/crate/webi_components/src/lib.rs index 93ca0af32..f5ac6ebbb 100644 --- a/crate/webi_components/src/lib.rs +++ b/crate/webi_components/src/lib.rs @@ -2,7 +2,12 @@ //! Web interface components for the peace automation framework. -pub use crate::{flow_graph::FlowGraph, home::Home}; +pub use crate::{ + children_fn::ChildrenFn, flow_graph::FlowGraph, flow_graph_current::FlowGraphCurrent, + home::Home, +}; +mod children_fn; mod flow_graph; +mod flow_graph_current; mod home; diff --git a/crate/webi_model/Cargo.toml b/crate/webi_model/Cargo.toml index 10ad966c3..ddf194f99 100644 --- a/crate/webi_model/Cargo.toml +++ b/crate/webi_model/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "peace_webi_model" description = "Web interface data types for the peace automation framework." -documentation = "https://docs.rs/peace_webi/" +documentation = "https://docs.rs/peace_webi_model/" version.workspace = true authors.workspace = true edition.workspace = true @@ -20,8 +20,22 @@ doctest = true test = false [dependencies] +cfg-if = { workspace = true } +dot_ix_model = { workspace = true } +indexmap = { workspace = true, features = ["serde"] } miette = { workspace = true, optional = true } +peace_core = { workspace = true } +peace_cmd_model = { workspace = true } +peace_item_model = { workspace = true, optional = true } +serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } [features] +default = [] error_reporting = ["dep:miette"] +output_progress = [ + "dep:peace_item_model", + "peace_core/output_progress", + "peace_cmd_model/output_progress", + "peace_item_model/output_progress", +] diff --git a/crate/webi_model/src/flow_info_graphs.rs b/crate/webi_model/src/flow_info_graphs.rs new file mode 100644 index 000000000..95cb5ab91 --- /dev/null +++ b/crate/webi_model/src/flow_info_graphs.rs @@ -0,0 +1,45 @@ +use std::{ + collections::HashMap, + ops::{Deref, DerefMut}, + sync::{Arc, Mutex}, +}; + +use dot_ix_model::info_graph::InfoGraph; + +/// Shared memory for `Map`. +/// +/// This may be used for example/actual outcome state. +#[derive(Clone, Debug)] +pub struct FlowInfoGraphs(Arc>>); + +impl FlowInfoGraphs { + /// Returns a new `FlowInfoGraphs` map. + pub fn new() -> Self { + Self::default() + } + + /// Returns the underlying `Arc>>`. + pub fn into_inner(self) -> Arc>> { + self.0 + } +} + +impl Deref for FlowInfoGraphs { + type Target = Arc>>; + + fn deref(&self) -> &Arc>> { + &self.0 + } +} + +impl DerefMut for FlowInfoGraphs { + fn deref_mut(&mut self) -> &mut Arc>> { + &mut self.0 + } +} + +impl Default for FlowInfoGraphs { + fn default() -> Self { + Self(Arc::new(Mutex::new(HashMap::new()))) + } +} diff --git a/crate/webi_model/src/flow_outcome_info_graphs.rs b/crate/webi_model/src/flow_outcome_info_graphs.rs new file mode 100644 index 000000000..741c3eb1f --- /dev/null +++ b/crate/webi_model/src/flow_outcome_info_graphs.rs @@ -0,0 +1,41 @@ +use std::ops::{Deref, DerefMut}; + +use crate::FlowInfoGraphs; + +/// Shared memory for `Map`. +/// +/// This is intended to be used for example / actual outcome diagrams. +#[derive(Clone, Debug)] +pub struct FlowOutcomeInfoGraphs(FlowInfoGraphs); + +impl FlowOutcomeInfoGraphs { + /// Returns a new `FlowOutcomeInfoGraphs` map. + pub fn new() -> Self { + Self::default() + } + + /// Returns the underlying `FlowInfoGraphs`. + pub fn into_inner(self) -> FlowInfoGraphs { + self.0 + } +} + +impl Deref for FlowOutcomeInfoGraphs { + type Target = FlowInfoGraphs; + + fn deref(&self) -> &FlowInfoGraphs { + &self.0 + } +} + +impl DerefMut for FlowOutcomeInfoGraphs { + fn deref_mut(&mut self) -> &mut FlowInfoGraphs { + &mut self.0 + } +} + +impl Default for FlowOutcomeInfoGraphs { + fn default() -> Self { + Self(Default::default()) + } +} diff --git a/crate/webi_model/src/flow_progress_info_graphs.rs b/crate/webi_model/src/flow_progress_info_graphs.rs new file mode 100644 index 000000000..d2011e8fb --- /dev/null +++ b/crate/webi_model/src/flow_progress_info_graphs.rs @@ -0,0 +1,41 @@ +use std::ops::{Deref, DerefMut}; + +use crate::FlowInfoGraphs; + +/// Shared memory for `Map`. +/// +/// This is intended to be used for progress diagrams. +#[derive(Clone, Debug)] +pub struct FlowProgressInfoGraphs(FlowInfoGraphs); + +impl FlowProgressInfoGraphs { + /// Returns a new `FlowProgressInfoGraphs` map. + pub fn new() -> Self { + Self::default() + } + + /// Returns the underlying `FlowInfoGraphs`. + pub fn into_inner(self) -> FlowInfoGraphs { + self.0 + } +} + +impl Deref for FlowProgressInfoGraphs { + type Target = FlowInfoGraphs; + + fn deref(&self) -> &FlowInfoGraphs { + &self.0 + } +} + +impl DerefMut for FlowProgressInfoGraphs { + fn deref_mut(&mut self) -> &mut FlowInfoGraphs { + &mut self.0 + } +} + +impl Default for FlowProgressInfoGraphs { + fn default() -> Self { + Self(Default::default()) + } +} diff --git a/crate/webi_model/src/lib.rs b/crate/webi_model/src/lib.rs index 61b508775..d88e1232c 100644 --- a/crate/webi_model/src/lib.rs +++ b/crate/webi_model/src/lib.rs @@ -1,5 +1,17 @@ //! Web interface data types for the peace automation framework. -pub use crate::webi_error::WebiError; +pub use crate::{ + flow_info_graphs::FlowInfoGraphs, flow_outcome_info_graphs::FlowOutcomeInfoGraphs, + flow_progress_info_graphs::FlowProgressInfoGraphs, + outcome_info_graph_variant::OutcomeInfoGraphVariant, + progress_info_graph_variant::ProgressInfoGraphVariant, web_ui_update::WebUiUpdate, + webi_error::WebiError, +}; +mod flow_info_graphs; +mod flow_outcome_info_graphs; +mod flow_progress_info_graphs; +mod outcome_info_graph_variant; +mod progress_info_graph_variant; +mod web_ui_update; mod webi_error; diff --git a/crate/webi_model/src/outcome_info_graph_variant.rs b/crate/webi_model/src/outcome_info_graph_variant.rs new file mode 100644 index 000000000..67eea5f6e --- /dev/null +++ b/crate/webi_model/src/outcome_info_graph_variant.rs @@ -0,0 +1,36 @@ +use serde::{Deserialize, Serialize}; + +cfg_if::cfg_if! { + if #[cfg(feature = "output_progress")] { + use std::collections::HashMap; + + use peace_core::{ + progress::{CmdBlockItemInteractionType, ProgressStatus}, + ItemId, + }; + use peace_item_model::ItemLocationState; + } +} + +/// How to style the outcome `InfoGraph`. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum OutcomeInfoGraphVariant { + /// Example `InfoGraph` diagram with no special styling. + Example, + /// Current `InfoGraph` diagram that shows execution progress. + Current { + /// Type of interactions that a `CmdBlock`s has with `ItemLocation`s. + #[cfg(feature = "output_progress")] + cmd_block_item_interaction_type: CmdBlockItemInteractionType, + /// `ItemLocationState`s of each item. + /// + /// This is used in the calculation for styling each node. + #[cfg(feature = "output_progress")] + item_location_states: HashMap, + /// Execution progress status of each item. + /// + /// This is used in the calculation for styling each edge. + #[cfg(feature = "output_progress")] + item_progress_statuses: HashMap, + }, +} diff --git a/crate/webi_model/src/progress_info_graph_variant.rs b/crate/webi_model/src/progress_info_graph_variant.rs new file mode 100644 index 000000000..1858bcfde --- /dev/null +++ b/crate/webi_model/src/progress_info_graph_variant.rs @@ -0,0 +1,25 @@ +use indexmap::IndexSet; +use peace_core::ItemId; +use serde::{Deserialize, Serialize}; + +/// How to style the progress `InfoGraph`. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum ProgressInfoGraphVariant { + /// Example `InfoGraph` diagram with no special styling. + Example, + /// Current `InfoGraph` diagram that shows execution progress. + Current { + /// IDs of items that are currently in progress. + /// + /// These items should be styled with animated blue strokes. + item_ids_in_progress: IndexSet, + /// IDs of the items that are already done. + /// + /// These items should be styled with green strokes. + item_ids_completed: IndexSet, + /// Whether the process is interrupted. + /// + /// Edges after items in progress should be styled yellow. + interrupted: bool, + }, +} diff --git a/crate/webi_model/src/web_ui_update.rs b/crate/webi_model/src/web_ui_update.rs new file mode 100644 index 000000000..e7efd53e5 --- /dev/null +++ b/crate/webi_model/src/web_ui_update.rs @@ -0,0 +1,55 @@ +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "output_progress")] +use peace_core::{ + progress::{CmdBlockItemInteractionType, ProgressLimit, ProgressStatus}, + ItemId, +}; +#[cfg(feature = "output_progress")] +use peace_item_model::ItemLocationState; + +/// A message that carries what needs to be updated in the web UI. +/// +/// This is received by the `CmdExecution` task, processed into `InfoGraph`, and +/// rendered by `leptos`. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub enum WebUiUpdate { + /// A `CmdBlock` has started. + #[cfg(feature = "output_progress")] + CmdBlockStart { + /// The type of interactions the `CmdBlock` has with the + /// `ItemLocation`s. + cmd_block_item_interaction_type: CmdBlockItemInteractionType, + }, + /// `ItemLocationState` for a single item. + /// + /// # Design Note + /// + /// `ItemLocationState` should live in `peace_item_model`, but this creates + /// a circular dependency. + #[cfg(feature = "output_progress")] + ItemLocationState { + /// ID of the `Item`. + item_id: ItemId, + /// The representation of the state of an `ItemLocation`. + item_location_state: ItemLocationState, + }, + /// Item's execution progress status. + #[cfg(feature = "output_progress")] + ItemProgressStatus { + /// ID of the item that is updated. + item_id: ItemId, + /// Status of the item's execution progress. + progress_status: ProgressStatus, + /// Progress limit for the execution, if known. + progress_limit: Option, + /// Message to display. + message: Option, + }, + /// Markdown to render. + Markdown { + /// The markdown source to render. + // TODO: receiver should render this using `pulldown-cmark`. + markdown_src: String, + }, +} diff --git a/crate/webi_output/Cargo.toml b/crate/webi_output/Cargo.toml index 42a3871f9..f8cae7987 100644 --- a/crate/webi_output/Cargo.toml +++ b/crate/webi_output/Cargo.toml @@ -22,27 +22,48 @@ test = false [dependencies] axum = { workspace = true } cfg-if = { workspace = true } +dot_ix_model = { workspace = true } futures = { workspace = true } +indexmap = { workspace = true } +interruptible = { workspace = true } leptos = { workspace = true } leptos_axum = { workspace = true } leptos_meta = { workspace = true } leptos_router = { workspace = true } -peace_core = { workspace = true, optional = true } +peace_cmd_model = { workspace = true } +peace_core = { workspace = true } peace_flow_model = { workspace = true } peace_fmt = { workspace = true } +peace_item_model = { workspace = true } +peace_params = { workspace = true } +peace_resource_rt = { workspace = true } +peace_rt_model = { 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"] } +smallvec = { workspace = true } +tokio = { workspace = true, features = ["net", "sync"] } tower-http = { workspace = true, features = ["fs"] } [features] default = [] +item_interactions = [ + "peace_rt_model/item_interactions", + "peace_webi_components/item_interactions", +] +item_state_example = [ + "peace_rt_model/item_state_example", + "peace_webi_components/item_state_example", +] output_progress = [ - "dep:peace_core", + "peace_cmd_model/output_progress", "peace_core/output_progress", + "peace_flow_model/output_progress", + "peace_item_model/output_progress", "peace_rt_model_core/output_progress", + "peace_rt_model/output_progress", + "peace_webi_model/output_progress", ] ssr = [ "leptos/ssr", diff --git a/crate/webi_output/src/cmd_exec_spawn_ctx.rs b/crate/webi_output/src/cmd_exec_spawn_ctx.rs new file mode 100644 index 000000000..1ddd7ef4d --- /dev/null +++ b/crate/webi_output/src/cmd_exec_spawn_ctx.rs @@ -0,0 +1,27 @@ +use std::fmt; + +use futures::future::LocalBoxFuture; +use interruptible::InterruptSignal; +use tokio::sync::mpsc; + +/// The `CmdExecution` task, as well as the channels to interact with it. +/// +/// This is returned by the `CmdExecution` spawning function for each `Flow`, +/// which is registered in `WebiOutput`. +pub struct CmdExecSpawnCtx { + /// Channel sender to send an `InterruptSignal`. + pub interrupt_tx: Option>, + /// The `*Cmd::run(..)` task. + /// + /// This will be submitted to the tokio task pool. + pub cmd_exec_task: LocalBoxFuture<'static, ()>, +} + +impl fmt::Debug for CmdExecSpawnCtx { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("CmdExecSpawnCtx") + .field("interrupt_tx", &self.interrupt_tx) + .field("cmd_exec_task", &stringify!(LocalBoxFuture<'static, ()>)) + .finish() + } +} diff --git a/crate/webi_output/src/cmd_exec_to_leptos_ctx.rs b/crate/webi_output/src/cmd_exec_to_leptos_ctx.rs new file mode 100644 index 000000000..5fac65bb7 --- /dev/null +++ b/crate/webi_output/src/cmd_exec_to_leptos_ctx.rs @@ -0,0 +1,63 @@ +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, +}; + +use interruptible::InterruptSignal; +use peace_cmd_model::CmdExecutionId; +use peace_core::FlowId; +use tokio::sync::mpsc; + +use peace_webi_model::{FlowOutcomeInfoGraphs, FlowProgressInfoGraphs}; + +/// The shared memory to write to to communicate between the `CmdExecution`s and +/// `leptos`. +#[derive(Clone, Debug, Default)] +pub struct CmdExecToLeptosCtx { + /// The example progress `InfoGraph` for all `CmdExecution`s. + /// + /// Shared memory for `Map`. + pub flow_progress_example_info_graphs: FlowProgressInfoGraphs, + /// The actual progress `InfoGraph` for all `CmdExecution`s. + /// + /// Shared memory for `Map`. + pub flow_progress_actual_info_graphs: FlowProgressInfoGraphs, + /// The example outcome `InfoGraph` for all `CmdExecution`s. + /// + /// Shared memory for `Map`. + pub flow_outcome_example_info_graphs: FlowOutcomeInfoGraphs, + /// The actual outcome `InfoGraph` for all `CmdExecution`s. + /// + /// Shared memory for `Map`. + pub flow_outcome_actual_info_graphs: FlowOutcomeInfoGraphs, + /// The interrupt channel sender for each `CmdExecution`. + pub cmd_exec_interrupt_txs: HashMap>, + /// The `cmd_execution_id` of the active `CmdExecution`. + /// + /// # Design + /// + /// This should go away, and instead be a value returned to the client and + /// stored in the URL. + pub cmd_execution_id: Arc>>, +} + +impl CmdExecToLeptosCtx { + /// Returns a new `CmdExecToLeptosCtx`. + pub fn new( + flow_progress_example_info_graphs: FlowProgressInfoGraphs, + flow_progress_actual_info_graphs: FlowProgressInfoGraphs, + flow_outcome_example_info_graphs: FlowOutcomeInfoGraphs, + flow_outcome_actual_info_graphs: FlowOutcomeInfoGraphs, + cmd_exec_interrupt_txs: HashMap>, + cmd_execution_id: Arc>>, + ) -> Self { + Self { + flow_progress_example_info_graphs, + flow_progress_actual_info_graphs, + flow_outcome_example_info_graphs, + flow_outcome_actual_info_graphs, + cmd_exec_interrupt_txs, + cmd_execution_id, + } + } +} diff --git a/crate/webi_output/src/flow_webi_fns.rs b/crate/webi_output/src/flow_webi_fns.rs new file mode 100644 index 000000000..44e21567f --- /dev/null +++ b/crate/webi_output/src/flow_webi_fns.rs @@ -0,0 +1,70 @@ +use std::fmt::{self, Debug}; + +use dot_ix_model::info_graph::InfoGraph; +use futures::future::LocalBoxFuture; +use peace_params::ParamsSpecs; +use peace_resource_rt::{resources::ts::SetUp, Resources}; +use peace_rt_model::Flow; + +use crate::{CmdExecSpawnCtx, WebiOutput}; + +/// Functions to work with `Flow` from the [`WebiOutput`]. +/// +/// [`WebiOutput`]: crate::WebiOutput +pub struct FlowWebiFns { + /// Flow to work with. + pub flow: Flow, + /// Function to create an `InfoGraph`. + /// + /// # Design + /// + /// This circumvents the need to pass around the specific `CmdCtx` type by + /// getting the tool developer to instantiate the `CmdCtx`, then pass the + /// relevant parameters to the function that we pass in. + #[allow(clippy::type_complexity)] + pub outcome_info_graph_fn: Box< + dyn Fn( + &mut WebiOutput, + Box, &ParamsSpecs, &Resources) -> InfoGraph>, + ) -> LocalBoxFuture, + >, + /// Function to spawn a `CmdExecution`. + /// + /// # Design + /// + /// Because passing around a `CmdCtx` with all its type parameters is + /// technically high cost, all of the `CmdCtx` instantiation logic, and + /// `*Cmd::run` invocations are hidden behind a plain function interface. + /// + /// Currently we only take in one function. In the future this should take + /// in a `Map` + pub cmd_exec_spawn_fn: Box CmdExecSpawnCtx>, +} + +impl fmt::Debug for FlowWebiFns +where + E: Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let cmd_exec_req_t_type_name = std::any::type_name::(); + + f.debug_struct("FlowWebiFns") + .field("flow", &self.flow) + .field( + "outcome_info_graph_fn", + &stringify!( + Box< + dyn Fn( + &mut WebiOutput, + Box, &ParamsSpecs, &Resources) -> InfoGraph>, + ) -> LocalBoxFuture, + > + ), + ) + .field( + "cmd_exec_spawn_fn", + &format!("Box CmdExecSpawnCtx>"), + ) + .finish() + } +} diff --git a/crate/webi_output/src/lib.rs b/crate/webi_output/src/lib.rs index 7921e88a9..e876f3ceb 100644 --- a/crate/webi_output/src/lib.rs +++ b/crate/webi_output/src/lib.rs @@ -1,8 +1,26 @@ //! Web interface output for the peace automation framework. -pub use crate::{webi_output::WebiOutput, webi_server::WebiServer}; +pub use crate::{ + cmd_exec_spawn_ctx::CmdExecSpawnCtx, cmd_exec_to_leptos_ctx::CmdExecToLeptosCtx, + flow_webi_fns::FlowWebiFns, webi_output::WebiOutput, webi_server::WebiServer, +}; + +#[cfg(feature = "item_interactions")] +pub use crate::outcome_info_graph_calculator::OutcomeInfoGraphCalculator; + +#[cfg(feature = "output_progress")] +pub use crate::progress_info_graph_calculator::ProgressInfoGraphCalculator; pub mod assets; +mod cmd_exec_spawn_ctx; +mod cmd_exec_to_leptos_ctx; +mod flow_webi_fns; mod webi_output; mod webi_server; + +#[cfg(feature = "item_interactions")] +mod outcome_info_graph_calculator; + +#[cfg(feature = "output_progress")] +mod progress_info_graph_calculator; diff --git a/crate/webi_output/src/outcome_info_graph_calculator.rs b/crate/webi_output/src/outcome_info_graph_calculator.rs new file mode 100644 index 000000000..26b459761 --- /dev/null +++ b/crate/webi_output/src/outcome_info_graph_calculator.rs @@ -0,0 +1,1580 @@ +use std::{collections::HashMap, marker::PhantomData, str::FromStr}; + +use dot_ix_model::{ + common::{ + graphviz_attrs::{EdgeDir, PackMode, PackModeFlag}, + AnyId, EdgeId, Edges, GraphvizAttrs, NodeHierarchy, NodeId, NodeNames, TagId, TagItems, + TagNames, TagStyles, + }, + info_graph::{GraphDir, InfoGraph}, + theme::{AnyIdOrDefaults, CssClassPartials, Theme, ThemeAttr, ThemeStyles}, +}; +use indexmap::IndexMap; +use peace_core::ItemId; +use peace_item_model::{ + ItemInteraction, ItemInteractionPull, ItemInteractionPush, ItemInteractionWithin, ItemLocation, + ItemLocationTree, ItemLocationType, ItemLocationsAndInteractions, +}; +use peace_params::ParamsSpecs; +use peace_resource_rt::{resources::ts::SetUp, Resources}; +use peace_rt_model::Flow; +use peace_webi_model::OutcomeInfoGraphVariant; +use smallvec::SmallVec; + +#[cfg(feature = "output_progress")] +use std::{collections::HashSet, ops::ControlFlow}; + +#[cfg(feature = "output_progress")] +use peace_core::progress::{CmdBlockItemInteractionType, ProgressComplete, ProgressStatus}; +#[cfg(feature = "output_progress")] +use peace_item_model::{ItemLocationState, ItemLocationStateInProgress}; + +/// Calculates the example / actual `InfoGraph` for a flow's outcome. +#[derive(Debug)] +pub struct OutcomeInfoGraphCalculator; + +impl OutcomeInfoGraphCalculator { + /// Returns the calculated `InfoGraph`. + pub fn calculate( + flow: &Flow, + params_specs: &ParamsSpecs, + resources: &Resources, + outcome_info_graph_variant: OutcomeInfoGraphVariant, + ) -> InfoGraph + where + E: 'static, + { + let item_locations_and_interactions = match &outcome_info_graph_variant { + OutcomeInfoGraphVariant::Example => { + flow.item_locations_and_interactions_example(params_specs, resources) + } + OutcomeInfoGraphVariant::Current { .. } => { + flow.item_locations_and_interactions_current(params_specs, resources) + } + }; + + calculate_info_graph( + flow, + outcome_info_graph_variant, + item_locations_and_interactions, + ) + } +} + +fn calculate_info_graph( + flow: &Flow, + outcome_info_graph_variant: OutcomeInfoGraphVariant, + item_locations_and_interactions: ItemLocationsAndInteractions, +) -> InfoGraph +where + E: 'static, +{ + let item_count = flow.graph().node_count(); + let ItemLocationsAndInteractions { + item_location_trees, + item_to_item_interactions, + item_location_count, + #[cfg(feature = "output_progress")] + item_location_to_item_id_sets, + } = item_locations_and_interactions; + + let node_id_mappings_and_hierarchy = node_id_mappings_and_hierarchy( + &item_location_trees, + item_location_count, + #[cfg(feature = "output_progress")] + &item_location_to_item_id_sets, + ); + let NodeIdMappingsAndHierarchy { + node_id_to_item_locations, + mut item_location_to_node_id_segments, + node_hierarchy, + #[cfg(feature = "output_progress")] + node_id_to_item_id_sets, + } = node_id_mappings_and_hierarchy; + + let node_names = node_id_to_item_locations.iter().fold( + NodeNames::with_capacity(item_location_count), + |mut node_names, (node_id, item_location)| { + node_names.insert(node_id.clone(), item_location.name().to_string()); + node_names + }, + ); + + let tags = match &outcome_info_graph_variant { + OutcomeInfoGraphVariant::Example => { + let tags = flow.graph().iter_insertion().fold( + TagNames::with_capacity(item_count), + |mut tags, item| { + let tag_name = item.interactions_tag_name(); + + // For some reason using `TagId::new(item.id().as_str())` causes an error to be + // highlighted on `flow.graph()`, rather than referring to `item.id()` as the + // cause of an extended borrow. + let item_id = item.id(); + let tag_id = TagId::try_from(format!("tag_{item_id}")) + .expect("Expected `tag_id` from `item_id` to be valid."); + + tags.insert(tag_id, tag_name); + + tags + }, + ); + + Some(tags) + } + OutcomeInfoGraphVariant::Current { .. } => None, + }; + + // 1. Each item interaction knows the `ItemLocation`s + // 2. We need to be able to translate from an `ItemLocation`, to the `NodeId`s + // that we need to link as edges. + // 3. We have a way to map from `ItemLocation` to `NodeId` using the + // `node_id_from_item_location` function. + // 4. So, either we calculate the `NodeId` from each `ItemLocation` in each + // interaction again, or `ItemLocation` must implement `Hash` and `Eq`, and + // look it up. + // 5. It already implements `Hash` and `Eq`, so let's construct a + // `Map`. + // 6. Then we can iterate through `item_to_item_interactions`, and for each + // `ItemLocation`, look up the map from 5, and add an edge. + let item_interactions_process_ctx = ItemInteractionsProcessCtx { + outcome_info_graph_variant: &outcome_info_graph_variant, + item_count, + item_location_count, + item_to_item_interactions: &item_to_item_interactions, + node_id_to_item_locations: &node_id_to_item_locations, + item_location_to_node_id_segments: &mut item_location_to_node_id_segments, + }; + let item_interactions_processed = process_item_interactions(item_interactions_process_ctx); + let ItemInteractionsProcessed { + edges, + graphviz_attrs, + mut theme, + tag_items, + tag_styles_focus, + } = item_interactions_processed; + + theme_styles_augment( + &item_location_trees, + &node_id_to_item_locations, + &mut theme, + #[cfg(feature = "output_progress")] + &outcome_info_graph_variant, + #[cfg(feature = "output_progress")] + &node_id_to_item_id_sets, + ); + + let mut info_graph = InfoGraph::default() + .with_direction(GraphDir::Vertical) + .with_hierarchy(node_hierarchy) + .with_node_names(node_names) + .with_edges(edges) + .with_graphviz_attrs(graphviz_attrs) + .with_theme(theme) + .with_css(String::from( + r#" +@keyframes node-stroke-dashoffset-move { + 0% { stroke-dashoffset: 0; } + 100% { stroke-dashoffset: 30; } +} +@keyframes stroke-dashoffset-move { + 0% { stroke-dashoffset: 136; } + 100% { stroke-dashoffset: 0; } +} +@keyframes stroke-dashoffset-move-request { + 0% { stroke-dashoffset: 0; } + 100% { stroke-dashoffset: 198; } +} +@keyframes stroke-dashoffset-move-response { + 0% { stroke-dashoffset: 0; } + 100% { stroke-dashoffset: -218; } +} +"#, + )); + + if let Some(tags) = tags { + info_graph = info_graph.with_tags(tags) + } + if let Some(tag_items) = tag_items { + info_graph = info_graph.with_tag_items(tag_items) + } + if let Some(tag_styles_focus) = tag_styles_focus { + info_graph = info_graph.with_tag_styles_focus(tag_styles_focus) + } + + info_graph +} + +/// Adds styles for nodes based on what kind of [`ItemLocation`] they represent, +/// and their progress status. +fn theme_styles_augment( + item_location_trees: &[ItemLocationTree], + node_id_to_item_locations: &IndexMap, + theme: &mut Theme, + #[cfg(feature = "output_progress")] outcome_info_graph_variant: &OutcomeInfoGraphVariant, + #[cfg(feature = "output_progress")] node_id_to_item_id_sets: &HashMap>, +) { + // Use light styling for `ItemLocationType::Group` nodes. + let mut css_class_partials_light = CssClassPartials::with_capacity(10); + css_class_partials_light.insert(ThemeAttr::StrokeStyle, "dotted".to_string()); + css_class_partials_light.insert(ThemeAttr::StrokeShadeNormal, "300".to_string()); + css_class_partials_light.insert(ThemeAttr::StrokeShadeHover, "300".to_string()); + css_class_partials_light.insert(ThemeAttr::StrokeShadeFocus, "400".to_string()); + css_class_partials_light.insert(ThemeAttr::StrokeShadeActive, "500".to_string()); + css_class_partials_light.insert(ThemeAttr::FillShadeNormal, "50".to_string()); + css_class_partials_light.insert(ThemeAttr::FillShadeHover, "50".to_string()); + css_class_partials_light.insert(ThemeAttr::FillShadeFocus, "100".to_string()); + css_class_partials_light.insert(ThemeAttr::FillShadeActive, "200".to_string()); + + node_id_to_item_locations + .iter() + .for_each(|(node_id, item_location)| { + let css_class_partials = match item_location.r#type() { + ItemLocationType::Host => { + // Specially colour some known hosts. + match item_location.name() { + ItemLocation::LOCALHOST => { + let mut css_class_partials = css_class_partials_light.clone(); + css_class_partials.insert(ThemeAttr::ShapeColor, "blue".to_string()); + + Some(css_class_partials) + } + "github.com" => { + let mut css_class_partials = css_class_partials_light.clone(); + css_class_partials.insert(ThemeAttr::ShapeColor, "purple".to_string()); + + Some(css_class_partials) + } + _ => { + // Not all hosts should be styled light -- only the ones that are top + // level. i.e. if the host is inside a group, then it should likely be + // styled darker. + if item_location_trees + .iter() + .map(ItemLocationTree::item_location) + .any(|item_location_top_level| { + item_location_top_level == *item_location + }) + { + Some(css_class_partials_light.clone()) + } else { + None + } + } + } + } + ItemLocationType::Group => Some(css_class_partials_light.clone()), + ItemLocationType::Path => { + #[cfg(not(feature = "output_progress"))] + { + None + } + + #[cfg(feature = "output_progress")] + { + if let OutcomeInfoGraphVariant::Current { + cmd_block_item_interaction_type, + item_location_states, + item_progress_statuses, + } = outcome_info_graph_variant + { + let cmd_block_item_interaction_type = *cmd_block_item_interaction_type; + // 1. For each of the item IDs that referred to this node + node_id_to_item_id_sets + .get(node_id) + // 2. Look up their statuses + .and_then(|referrer_item_ids| { + // When we have multiple referrers referring to the same item + // location, we need to prioritize `ItemLocationState::Exists` + // over `ItemLocationState::NotExists`. + // + // This is because: + // + // * For ensure, a predecessor would have created the item + // beforehand, so we don't want a successor's `NotExists` + // state to hide the node. e.g. a file download before + // uploading it somewhere else. + // + // * For clean, the successor's destination would be removed, + // but not its source. e.g. the upload would remove the + // destination file, and not the source, which would later be + // removed by the predecessor. + // + // Which means we need to prioritize the styles from the most + // recent completed / in-progress `referrer_item_id`. + let (ControlFlow::Continue(item_location_state) + | ControlFlow::Break(item_location_state)) = referrer_item_ids + .iter() + .filter_map(|referrer_item_id| { + item_location_states.get(referrer_item_id).copied() + }) + .try_fold( + ItemLocationState::NotExists, + |_item_location_state_acc, item_location_state| { + match item_location_state { + ItemLocationState::Exists => { + ControlFlow::Break( + ItemLocationState::Exists, + ) + } + ItemLocationState::NotExists => { + ControlFlow::Continue( + ItemLocationState::NotExists, + ) + } + } + }, + ); + + let (ControlFlow::Continue(progress_status) + | ControlFlow::Break(progress_status)) = referrer_item_ids + .iter() + .filter_map(|referrer_item_id| { + item_progress_statuses.get(referrer_item_id).cloned() + }) + .try_fold( + ProgressStatus::Initialized, + |_progress_status_acc, progress_status| { + match progress_status { + ProgressStatus::Initialized => { + ControlFlow::Continue(progress_status) + } + ProgressStatus::Interrupted => { + ControlFlow::Continue(progress_status) + } + ProgressStatus::ExecPending => { + ControlFlow::Continue(progress_status) + } + ProgressStatus::Queued => { + ControlFlow::Continue(progress_status) + } + ProgressStatus::Running + | ProgressStatus::RunningStalled + | ProgressStatus::UserPending => { + ControlFlow::Break(progress_status) + } + ProgressStatus::Complete( + ProgressComplete::Success, + ) => ControlFlow::Continue(progress_status), + ProgressStatus::Complete( + ProgressComplete::Fail, + ) => ControlFlow::Break(progress_status), + } + }, + ); + + node_css_class_partials( + cmd_block_item_interaction_type, + item_location_state, + progress_status, + ) + }) + } else { + None + } + } + } + }; + + if let Some(css_class_partials) = css_class_partials { + theme.styles.insert( + AnyIdOrDefaults::AnyId(AnyId::from(node_id.clone())), + css_class_partials, + ); + } + }); +} + +#[cfg(feature = "output_progress")] +fn node_css_class_partials( + cmd_block_item_interaction_type: CmdBlockItemInteractionType, + item_location_state: ItemLocationState, + progress_status: ProgressStatus, +) -> Option { + // 3. If any of them are running or complete, then it should be visible. + let item_location_state_in_progress = ItemLocationStateInProgress::from( + cmd_block_item_interaction_type, + item_location_state, + progress_status, + ); + + match item_location_state_in_progress { + ItemLocationStateInProgress::NotExists => { + let mut css_class_partials = CssClassPartials::with_capacity(1); + css_class_partials.insert(ThemeAttr::Extra, "opacity-[0.15]".to_string()); + Some(css_class_partials) + } + ItemLocationStateInProgress::NotExistsError => { + let mut css_class_partials = CssClassPartials::with_capacity(2); + css_class_partials.insert(ThemeAttr::ShapeColor, "red".to_string()); + css_class_partials.insert(ThemeAttr::StrokeStyle, "dashed".to_string()); + Some(css_class_partials) + } + ItemLocationStateInProgress::DiscoverInProgress => { + let mut css_class_partials = CssClassPartials::with_capacity(3); + css_class_partials.insert(ThemeAttr::ShapeColor, "yellow".to_string()); + css_class_partials.insert(ThemeAttr::StrokeStyle, "dashed".to_string()); + css_class_partials.insert( + ThemeAttr::Animate, + "[node-stroke-dashoffset-move_1s_linear_infinite]".to_string(), + ); + Some(css_class_partials) + } + ItemLocationStateInProgress::DiscoverError => { + let mut css_class_partials = CssClassPartials::with_capacity(3); + css_class_partials.insert(ThemeAttr::ShapeColor, "amber".to_string()); + css_class_partials.insert(ThemeAttr::StrokeStyle, "dashed".to_string()); + css_class_partials.insert( + ThemeAttr::Animate, + "[node-stroke-dashoffset-move_1s_linear_infinite]".to_string(), + ); + Some(css_class_partials) + } + ItemLocationStateInProgress::CreateInProgress => { + let mut css_class_partials = CssClassPartials::with_capacity(3); + css_class_partials.insert(ThemeAttr::ShapeColor, "blue".to_string()); + css_class_partials.insert(ThemeAttr::StrokeStyle, "dashed".to_string()); + css_class_partials.insert( + ThemeAttr::Animate, + "[node-stroke-dashoffset-move_1s_linear_infinite]".to_string(), + ); + Some(css_class_partials) + } + ItemLocationStateInProgress::ModificationInProgress => { + let mut css_class_partials = CssClassPartials::with_capacity(3); + css_class_partials.insert(ThemeAttr::ShapeColor, "blue".to_string()); + css_class_partials.insert(ThemeAttr::StrokeStyle, "dashed".to_string()); + css_class_partials.insert( + ThemeAttr::Animate, + "[node-stroke-dashoffset-move_1s_linear_infinite]".to_string(), + ); + Some(css_class_partials) + } + ItemLocationStateInProgress::ExistsOk => None, + ItemLocationStateInProgress::ExistsError => { + let mut css_class_partials = CssClassPartials::with_capacity(1); + css_class_partials.insert(ThemeAttr::ShapeColor, "red".to_string()); + Some(css_class_partials) + } + } +} + +/// Calculates edges and styles from `ItemInteraction`s. +/// +/// # Code +/// +/// Currently the code goes through the `ItemInteraction`s, and populates the +/// `Edges`, `Theme`, and `GraphvizAttrs`. This isn't as "clean" as iterating +/// over the `ItemInteraction`s per attribute that is to be computed, but +/// perhaps populating the different structures per `ItemInteraction` is more +/// manageable than remembering to update multiple functions. +fn process_item_interactions( + item_interactions_process_ctx: ItemInteractionsProcessCtx<'_, '_>, +) -> ItemInteractionsProcessed { + let ItemInteractionsProcessCtx { + outcome_info_graph_variant, + item_count, + item_location_count, + item_to_item_interactions, + node_id_to_item_locations, + item_location_to_node_id_segments, + } = item_interactions_process_ctx; + + let edges = Edges::with_capacity(item_location_count); + let mut graphviz_attrs = GraphvizAttrs::new().with_edge_minlen_default(3); + graphviz_attrs.pack_mode = PackMode::Array { + flags: vec![PackModeFlag::T], + number: None, + }; + let mut theme = Theme::new(); + theme.styles.insert(AnyIdOrDefaults::EdgeDefaults, { + let mut css_class_partials = CssClassPartials::with_capacity(1); + css_class_partials.insert(ThemeAttr::Visibility, "invisible".to_string()); + css_class_partials + }); + + match outcome_info_graph_variant { + OutcomeInfoGraphVariant::Example => { + let item_interactions_processed_example = ItemInteractionsProcessedExample { + edges, + graphviz_attrs, + tag_items: TagItems::with_capacity(item_count), + tag_styles_focus: TagStyles::new(), + }; + + let item_interactions_processed_example = process_item_interactions_example( + item_to_item_interactions, + item_interactions_processed_example, + node_id_to_item_locations, + item_location_to_node_id_segments, + ); + let ItemInteractionsProcessedExample { + edges, + graphviz_attrs, + tag_items, + tag_styles_focus, + } = item_interactions_processed_example; + + ItemInteractionsProcessed { + edges, + graphviz_attrs, + theme, + tag_items: Some(tag_items), + tag_styles_focus: Some(tag_styles_focus), + } + } + OutcomeInfoGraphVariant::Current { + #[cfg(feature = "output_progress")] + cmd_block_item_interaction_type: _, + #[cfg(feature = "output_progress")] + item_location_states: _, + #[cfg(feature = "output_progress")] + item_progress_statuses, + } => { + let item_interactions_processed_current = ItemInteractionsProcessedCurrent { + edges, + graphviz_attrs, + theme, + #[cfg(feature = "output_progress")] + item_progress_statuses, + marker: PhantomData, + }; + + let item_interactions_processed_current = process_item_interactions_current( + item_to_item_interactions, + item_interactions_processed_current, + node_id_to_item_locations, + item_location_to_node_id_segments, + ); + + let ItemInteractionsProcessedCurrent { + edges, + graphviz_attrs, + theme, + #[cfg(feature = "output_progress")] + item_progress_statuses: _, + marker: PhantomData, + } = item_interactions_processed_current; + + ItemInteractionsProcessed { + edges, + graphviz_attrs, + theme, + tag_items: None, + tag_styles_focus: None, + } + } + } +} + +/// Processes `ItemInteraction`s from all items for an example `InfoGraph` +/// diagram. +/// +/// This means: +/// +/// 1. Each node should be fully visible. +/// 2. Edges should be visible when a tag is clicked. +fn process_item_interactions_example<'item_location>( + item_to_item_interactions: &'item_location IndexMap>, + item_interactions_processed_example: ItemInteractionsProcessedExample, + node_id_to_item_locations: &IndexMap, + item_location_to_node_id_segments: &mut HashMap<&'item_location ItemLocation, String>, +) -> ItemInteractionsProcessedExample { + item_to_item_interactions + .iter() + // The capacity could be worked out through the sum of all `ItemInteraction`s. + // + // For now we just use the `item_location_count` as a close approximation. + .fold( + item_interactions_processed_example, + // Use `item_id` to compute `tags` and `tag_items`. + |item_interactions_processed_example, (item_id, item_interactions)| { + let ItemInteractionsProcessedExample { + mut edges, + mut graphviz_attrs, + mut tag_items, + mut tag_styles_focus, + } = item_interactions_processed_example; + + let tag_id = TagId::try_from(format!("tag_{item_id}")) + .expect("Expected `tag_id` from `item_id` to be valid."); + let tag_id = &tag_id; + + item_interactions.iter().for_each(|item_interaction| { + let item_interactions_processing_ctx = ItemInteractionsProcessingCtxExample { + node_id_to_item_locations, + item_location_to_node_id_segments, + edges: &mut edges, + tag_items: &mut tag_items, + tag_id, + tag_styles_focus: &mut tag_styles_focus, + }; + + match item_interaction { + ItemInteraction::Push(item_interaction_push) => { + process_item_interaction_push_example( + item_interactions_processing_ctx, + item_interaction_push, + ); + } + ItemInteraction::Pull(item_interaction_pull) => { + process_item_interaction_pull_example( + item_interactions_processing_ctx, + &mut graphviz_attrs, + item_interaction_pull, + ); + } + ItemInteraction::Within(item_interaction_within) => { + process_item_interaction_within_example( + item_interactions_processing_ctx, + item_interaction_within, + ); + } + } + }); + + ItemInteractionsProcessedExample { + edges, + graphviz_attrs, + tag_items, + tag_styles_focus, + } + }, + ) +} + +/// Inserts an edge between the `from` and `to` nodes of an +/// [`ItemInteractionPush`]. +fn process_item_interaction_push_example<'item_location>( + item_interactions_processing_ctx: ItemInteractionsProcessingCtxExample<'_, 'item_location>, + item_interaction_push: &'item_location ItemInteractionPush, +) { + let ItemInteractionsProcessingCtxExample { + node_id_to_item_locations, + item_location_to_node_id_segments, + edges, + tag_items, + tag_id, + tag_styles_focus, + } = item_interactions_processing_ctx; + // Use the innermost node from the interaction. + // The `NodeId` for the item location is the longest node ID that contains all + // of the `node_id_segment`s of the selected item location's ancestors. + let node_id_from = { + let node_id_from = node_id_from_item_location(item_location_to_node_id_segments, || { + item_interaction_push.location_from().iter() + }); + + node_id_with_ancestor_find(node_id_to_item_locations, node_id_from) + }; + + // Use the innermost node. + let node_id_to = { + let node_id_to = node_id_from_item_location(item_location_to_node_id_segments, || { + item_interaction_push.location_to().iter() + }); + + node_id_with_ancestor_find(node_id_to_item_locations, node_id_to) + }; + + let edge_id = EdgeId::from_str(&format!("{node_id_from}___{node_id_to}")) + .expect("Expected edge ID from item location ID to be valid for `edge_id`."); + edges.insert(edge_id.clone(), [node_id_from.clone(), node_id_to.clone()]); + + if let Some(any_ids) = tag_items.get_mut(tag_id) { + any_ids.push(AnyId::from(node_id_from.clone())); + any_ids.push(AnyId::from(node_id_to.clone())); + any_ids.push(AnyId::from(edge_id.clone())); + } else { + let any_ids = vec![ + AnyId::from(node_id_from.clone()), + AnyId::from(node_id_to.clone()), + AnyId::from(edge_id.clone()), + ]; + tag_items.insert(tag_id.clone(), any_ids); + } + + let css_class_partials = item_interaction_push_css_class_partials(true); + + if let Some(theme_styles) = tag_styles_focus.get_mut(tag_id) { + theme_styles.insert( + AnyIdOrDefaults::AnyId(AnyId::from(edge_id)), + css_class_partials, + ); + } else { + let mut theme_styles = ThemeStyles::with_capacity(1); + theme_styles.insert( + AnyIdOrDefaults::AnyId(AnyId::from(edge_id)), + css_class_partials, + ); + tag_styles_focus.insert(tag_id.clone(), theme_styles); + } +} + +/// Inserts an edge between the `client` and `server` nodes of an +/// [`ItemInteractionPull`]. +fn process_item_interaction_pull_example<'item_location>( + item_interactions_processing_ctx: ItemInteractionsProcessingCtxExample<'_, 'item_location>, + graphviz_attrs: &mut GraphvizAttrs, + item_interaction_pull: &'item_location ItemInteractionPull, +) { + let ItemInteractionsProcessingCtxExample { + node_id_to_item_locations, + item_location_to_node_id_segments, + edges, + tag_items, + tag_id, + tag_styles_focus, + } = item_interactions_processing_ctx; + + // Use the outermost `ItemLocationType::Host` node. + let node_id_client = { + let item_location_ancestors_iter = || { + let mut host_found = false; + let mut location_from_iter = item_interaction_pull.location_client().iter(); + std::iter::from_fn(move || { + if host_found { + return None; + } + + let item_location = location_from_iter.next(); + if let Some(item_location) = item_location.as_ref() { + host_found = item_location.r#type() == ItemLocationType::Host; + } + item_location + }) + .fuse() + }; + + let node_id_client = node_id_from_item_location( + item_location_to_node_id_segments, + item_location_ancestors_iter, + ); + + node_id_with_ancestor_find(node_id_to_item_locations, node_id_client) + }; + + // Use the innermost node, as that's where the file is written to. + let node_id_client_file = { + let node_id_client_file = + node_id_from_item_location(item_location_to_node_id_segments, || { + item_interaction_pull.location_client().iter() + }); + + node_id_with_ancestor_find(node_id_to_item_locations, node_id_client_file) + }; + + // Use the innermost node. + let node_id_server = { + let node_id_server = node_id_from_item_location(item_location_to_node_id_segments, || { + item_interaction_pull.location_server().iter() + }); + + node_id_with_ancestor_find(node_id_to_item_locations, node_id_server) + }; + + let edge_id_request = + EdgeId::from_str(&format!("{node_id_client}___{node_id_server}___request")) + .expect("Expected edge ID from item location ID to be valid for `edge_id_request`."); + edges.insert( + edge_id_request.clone(), + [node_id_server.clone(), node_id_client.clone()], + ); + + let edge_id_response = EdgeId::from_str(&format!( + "{node_id_client_file}___{node_id_server}___response" + )) + .expect("Expected edge ID from item location ID to be valid for `edge_id_response`."); + edges.insert( + edge_id_response.clone(), + [node_id_server.clone(), node_id_client_file.clone()], + ); + + graphviz_attrs + .edge_dirs + .insert(edge_id_request.clone(), EdgeDir::Back); + + let css_class_partials_request = item_interaction_pull_request_css_class_partials(true); + let css_class_partials_response = item_interaction_pull_response_css_class_partials(true); + + if let Some(any_ids) = tag_items.get_mut(tag_id) { + any_ids.push(AnyId::from(node_id_server.clone())); + any_ids.push(AnyId::from(node_id_client_file.clone())); + any_ids.push(AnyId::from(edge_id_request.clone())); + any_ids.push(AnyId::from(edge_id_response.clone())); + } else { + let any_ids = vec![ + AnyId::from(node_id_server.clone()), + AnyId::from(node_id_client_file.clone()), + AnyId::from(edge_id_request.clone()), + AnyId::from(edge_id_response.clone()), + ]; + tag_items.insert(tag_id.clone(), any_ids); + } + + if let Some(theme_styles) = tag_styles_focus.get_mut(tag_id) { + theme_styles.insert( + AnyIdOrDefaults::AnyId(AnyId::from(edge_id_request)), + css_class_partials_request, + ); + theme_styles.insert( + AnyIdOrDefaults::AnyId(AnyId::from(edge_id_response)), + css_class_partials_response, + ); + } else { + let mut theme_styles = ThemeStyles::with_capacity(2); + theme_styles.insert( + AnyIdOrDefaults::AnyId(AnyId::from(edge_id_request)), + css_class_partials_request, + ); + theme_styles.insert( + AnyIdOrDefaults::AnyId(AnyId::from(edge_id_response)), + css_class_partials_response, + ); + tag_styles_focus.insert(tag_id.clone(), theme_styles); + } +} + +/// Indicates the nodes that are being waited upon by [`ItemInteractionWithin`]. +fn process_item_interaction_within_example<'item_location>( + item_interactions_processing_ctx: ItemInteractionsProcessingCtxExample<'_, 'item_location>, + item_interaction_within: &'item_location ItemInteractionWithin, +) { + let ItemInteractionsProcessingCtxExample { + node_id_to_item_locations, + item_location_to_node_id_segments, + edges: _, + tag_items, + tag_id, + tag_styles_focus, + } = item_interactions_processing_ctx; + + // Use the outermost `ItemLocationType::Host` node. + let node_id = { + let item_location_ancestors_iter = || { + let mut host_found = false; + let mut location_from_iter = item_interaction_within.location().iter(); + std::iter::from_fn(move || { + if host_found { + return None; + } + + let item_location = location_from_iter.next(); + if let Some(item_location) = item_location.as_ref() { + host_found = item_location.r#type() == ItemLocationType::Host; + } + item_location + }) + .fuse() + }; + + let node_id_client = node_id_from_item_location( + item_location_to_node_id_segments, + item_location_ancestors_iter, + ); + + node_id_with_ancestor_find(node_id_to_item_locations, node_id_client) + }; + + let css_class_partials = item_interaction_within_css_class_partials(); + + if let Some(any_ids) = tag_items.get_mut(tag_id) { + any_ids.push(AnyId::from(node_id.clone())); + } else { + let any_ids = vec![AnyId::from(node_id.clone())]; + tag_items.insert(tag_id.clone(), any_ids); + } + + if let Some(theme_styles) = tag_styles_focus.get_mut(tag_id) { + theme_styles.insert( + AnyIdOrDefaults::AnyId(AnyId::from(node_id)), + css_class_partials, + ); + } else { + let mut theme_styles = ThemeStyles::with_capacity(1); + theme_styles.insert( + AnyIdOrDefaults::AnyId(AnyId::from(node_id)), + css_class_partials, + ); + tag_styles_focus.insert(tag_id.clone(), theme_styles); + } +} + +/// Inserts an edge between the `from` and `to` nodes of an +/// [`ItemInteractionPush`]. +fn process_item_interaction_push_current<'item_location>( + item_interactions_processing_ctx: ItemInteractionsProcessingCtxCurrent<'_, 'item_location>, + item_interaction_push: &'item_location ItemInteractionPush, +) { + let ItemInteractionsProcessingCtxCurrent { + node_id_to_item_locations, + item_location_to_node_id_segments, + edges, + theme, + #[cfg(feature = "output_progress")] + progress_status, + } = item_interactions_processing_ctx; + // Use the innermost node from the interaction. + // The `NodeId` for the item location is the longest node ID that contains all + // of the `node_id_segment`s of the selected item location's ancestors. + let node_id_from = { + let node_id_from = node_id_from_item_location(item_location_to_node_id_segments, || { + item_interaction_push.location_from().iter() + }); + + node_id_with_ancestor_find(node_id_to_item_locations, node_id_from) + }; + + // Use the innermost node. + let node_id_to = { + let node_id_to = node_id_from_item_location(item_location_to_node_id_segments, || { + item_interaction_push.location_to().iter() + }); + + node_id_with_ancestor_find(node_id_to_item_locations, node_id_to) + }; + + let edge_id = EdgeId::from_str(&format!("{node_id_from}___{node_id_to}")) + .expect("Expected edge ID from item location ID to be valid for `edge_id`."); + edges.insert(edge_id.clone(), [node_id_from.clone(), node_id_to.clone()]); + + #[cfg(feature = "output_progress")] + let edge_visible = matches!( + progress_status, + ProgressStatus::Running | ProgressStatus::RunningStalled | ProgressStatus::UserPending + ); + #[cfg(not(feature = "output_progress"))] + let edge_visible = false; + let css_class_partials = item_interaction_push_css_class_partials(edge_visible); + + theme.styles.insert( + AnyIdOrDefaults::AnyId(AnyId::from(edge_id)), + css_class_partials, + ); +} + +/// Inserts an edge between the `client` and `server` nodes of an +/// [`ItemInteractionPull`]. +fn process_item_interaction_pull_current<'item_location>( + item_interactions_processing_ctx: ItemInteractionsProcessingCtxCurrent<'_, 'item_location>, + graphviz_attrs: &mut GraphvizAttrs, + item_interaction_pull: &'item_location ItemInteractionPull, +) { + let ItemInteractionsProcessingCtxCurrent { + node_id_to_item_locations, + item_location_to_node_id_segments, + edges, + theme, + #[cfg(feature = "output_progress")] + progress_status, + } = item_interactions_processing_ctx; + + // Use the outermost `ItemLocationType::Host` node. + let node_id_client = { + let item_location_ancestors_iter = || { + let mut host_found = false; + let mut location_from_iter = item_interaction_pull.location_client().iter(); + std::iter::from_fn(move || { + if host_found { + return None; + } + + let item_location = location_from_iter.next(); + if let Some(item_location) = item_location.as_ref() { + host_found = item_location.r#type() == ItemLocationType::Host; + } + item_location + }) + .fuse() + }; + + let node_id_client = node_id_from_item_location( + item_location_to_node_id_segments, + item_location_ancestors_iter, + ); + + node_id_with_ancestor_find(node_id_to_item_locations, node_id_client) + }; + + // Use the innermost node, as that's where the file is written to. + let node_id_client_file = { + let node_id_client_file = + node_id_from_item_location(item_location_to_node_id_segments, || { + item_interaction_pull.location_client().iter() + }); + + node_id_with_ancestor_find(node_id_to_item_locations, node_id_client_file) + }; + + // Use the innermost node. + let node_id_server = { + let node_id_server = node_id_from_item_location(item_location_to_node_id_segments, || { + item_interaction_pull.location_server().iter() + }); + + node_id_with_ancestor_find(node_id_to_item_locations, node_id_server) + }; + + let edge_id_request = + EdgeId::from_str(&format!("{node_id_client}___{node_id_server}___request")) + .expect("Expected edge ID from item location ID to be valid for `edge_id_request`."); + edges.insert( + edge_id_request.clone(), + [node_id_server.clone(), node_id_client.clone()], + ); + + let edge_id_response = EdgeId::from_str(&format!( + "{node_id_client_file}___{node_id_server}___response" + )) + .expect("Expected edge ID from item location ID to be valid for `edge_id_response`."); + edges.insert( + edge_id_response.clone(), + [node_id_server.clone(), node_id_client_file.clone()], + ); + + graphviz_attrs + .edge_dirs + .insert(edge_id_request.clone(), EdgeDir::Back); + + #[cfg(feature = "output_progress")] + let edge_visible = matches!( + progress_status, + ProgressStatus::Running | ProgressStatus::RunningStalled | ProgressStatus::UserPending + ); + #[cfg(not(feature = "output_progress"))] + let edge_visible = false; + let css_class_partials_request = item_interaction_pull_request_css_class_partials(edge_visible); + let css_class_partials_response = + item_interaction_pull_response_css_class_partials(edge_visible); + + theme.styles.insert( + AnyIdOrDefaults::AnyId(AnyId::from(edge_id_request)), + css_class_partials_request, + ); + theme.styles.insert( + AnyIdOrDefaults::AnyId(AnyId::from(edge_id_response)), + css_class_partials_response, + ); +} + +/// Indicates the nodes that are being waited upon by [`ItemInteractionWithin`]. +fn process_item_interaction_within_current<'item_location>( + item_interactions_processing_ctx: ItemInteractionsProcessingCtxCurrent<'_, 'item_location>, + item_interaction_within: &'item_location ItemInteractionWithin, +) { + let ItemInteractionsProcessingCtxCurrent { + node_id_to_item_locations, + item_location_to_node_id_segments, + edges: _, + theme, + #[cfg(feature = "output_progress")] + progress_status, + } = item_interactions_processing_ctx; + + // Use the outermost `ItemLocationType::Host` node. + let node_id = { + let item_location_ancestors_iter = || { + let mut host_found = false; + let mut location_from_iter = item_interaction_within.location().iter(); + std::iter::from_fn(move || { + if host_found { + return None; + } + + let item_location = location_from_iter.next(); + if let Some(item_location) = item_location.as_ref() { + host_found = item_location.r#type() == ItemLocationType::Host; + } + item_location + }) + .fuse() + }; + + let node_id_client = node_id_from_item_location( + item_location_to_node_id_segments, + item_location_ancestors_iter, + ); + + node_id_with_ancestor_find(node_id_to_item_locations, node_id_client) + }; + + #[cfg(feature = "output_progress")] + let animate_node = matches!( + progress_status, + ProgressStatus::Running | ProgressStatus::RunningStalled | ProgressStatus::UserPending + ); + #[cfg(not(feature = "output_progress"))] + let animate_node = false; + if animate_node { + let css_class_partials = item_interaction_within_css_class_partials(); + + theme.styles.insert( + AnyIdOrDefaults::AnyId(AnyId::from(node_id)), + css_class_partials, + ); + } +} + +/// Processes `ItemInteraction`s from all items for an example `InfoGraph` +/// diagram. +/// +/// This means: +/// +/// 1. Each node should be fully visible. +/// 2. Edges should be visible when a tag is clicked. +fn process_item_interactions_current<'item_state, 'item_location>( + item_to_item_interactions: &'item_location IndexMap>, + item_interactions_processed_current: ItemInteractionsProcessedCurrent<'item_state>, + node_id_to_item_locations: &IndexMap, + item_location_to_node_id_segments: &mut HashMap<&'item_location ItemLocation, String>, +) -> ItemInteractionsProcessedCurrent<'item_state> { + item_to_item_interactions + .iter() + // The capacity could be worked out through the sum of all `ItemInteraction`s. + // + // For now we just use the `item_location_count` as a close approximation. + .fold( + item_interactions_processed_current, + |item_interactions_processed_current, (item_id, item_interactions)| { + let ItemInteractionsProcessedCurrent { + mut edges, + mut graphviz_attrs, + mut theme, + #[cfg(feature = "output_progress")] + item_progress_statuses, + marker: PhantomData, + } = item_interactions_processed_current; + + #[cfg(feature = "output_progress")] + let progress_status = item_progress_statuses + .get(item_id) + .cloned() + .unwrap_or(ProgressStatus::Initialized); + + #[cfg(not(feature = "output_progress"))] + let _item_id = item_id; + + item_interactions.iter().for_each(|item_interaction| { + #[cfg(feature = "output_progress")] + let progress_status = progress_status.clone(); + + let item_interactions_processing_ctx = ItemInteractionsProcessingCtxCurrent { + node_id_to_item_locations, + item_location_to_node_id_segments, + edges: &mut edges, + theme: &mut theme, + #[cfg(feature = "output_progress")] + progress_status, + }; + + match item_interaction { + ItemInteraction::Push(item_interaction_push) => { + process_item_interaction_push_current( + item_interactions_processing_ctx, + item_interaction_push, + ); + } + ItemInteraction::Pull(item_interaction_pull) => { + process_item_interaction_pull_current( + item_interactions_processing_ctx, + &mut graphviz_attrs, + item_interaction_pull, + ); + } + ItemInteraction::Within(item_interaction_within) => { + process_item_interaction_within_current( + item_interactions_processing_ctx, + item_interaction_within, + ); + } + } + }); + + ItemInteractionsProcessedCurrent { + edges, + graphviz_attrs, + theme, + #[cfg(feature = "output_progress")] + item_progress_statuses, + marker: PhantomData, + } + }, + ) +} + +/// Returns [`CssClassPartials`] for the edge between the `from` and `to` +/// [`ItemLocation`]s of an [`ItemInteractionPush`]. +fn item_interaction_push_css_class_partials(visible: bool) -> CssClassPartials { + let mut css_class_partials = CssClassPartials::with_capacity(6); + if visible { + css_class_partials.insert(ThemeAttr::Visibility, "visible".to_string()); + } + css_class_partials.insert( + ThemeAttr::Animate, + "[stroke-dashoffset-move_1s_linear_infinite]".to_string(), + ); + css_class_partials.insert(ThemeAttr::ShapeColor, "blue".to_string()); + css_class_partials.insert( + ThemeAttr::StrokeStyle, + "dasharray:0,40,1,2,1,2,2,2,4,2,8,2,20,50".to_string(), + ); + css_class_partials.insert(ThemeAttr::StrokeShadeNormal, "600".to_string()); + css_class_partials.insert(ThemeAttr::FillShadeNormal, "500".to_string()); + css_class_partials +} + +/// Returns [`CssClassPartials`] for the edge for the `client` to `server` +/// [`ItemLocation`] of an [`ItemInteractionPull`]. +fn item_interaction_pull_request_css_class_partials(visible: bool) -> CssClassPartials { + let mut css_class_partials_request = CssClassPartials::with_capacity(7); + if visible { + css_class_partials_request.insert(ThemeAttr::Visibility, "visible".to_string()); + } + css_class_partials_request.insert( + ThemeAttr::Animate, + "[stroke-dashoffset-move-request_1.5s_linear_infinite]".to_string(), + ); + css_class_partials_request.insert(ThemeAttr::ShapeColor, "blue".to_string()); + css_class_partials_request.insert( + ThemeAttr::StrokeStyle, + "dasharray:0,50,12,2,4,2,2,2,1,2,1,120".to_string(), + ); + css_class_partials_request.insert(ThemeAttr::StrokeWidth, "[1px]".to_string()); + css_class_partials_request.insert(ThemeAttr::StrokeShadeNormal, "600".to_string()); + css_class_partials_request.insert(ThemeAttr::FillShadeNormal, "500".to_string()); + css_class_partials_request +} + +/// Returns [`CssClassPartials`] for the edge for the `server` to `client` +/// [`ItemLocation`] of an [`ItemInteractionPull`]. +fn item_interaction_pull_response_css_class_partials(visible: bool) -> CssClassPartials { + let mut css_class_partials_response = CssClassPartials::with_capacity(7); + if visible { + css_class_partials_response.insert(ThemeAttr::Visibility, "visible".to_string()); + } + css_class_partials_response.insert( + ThemeAttr::Animate, + "[stroke-dashoffset-move-response_1.5s_linear_infinite]".to_string(), + ); + css_class_partials_response.insert(ThemeAttr::ShapeColor, "blue".to_string()); + css_class_partials_response.insert( + ThemeAttr::StrokeStyle, + "dasharray:0,120,1,2,1,2,2,2,4,2,8,2,20,50".to_string(), + ); + css_class_partials_response.insert(ThemeAttr::StrokeWidth, "[2px]".to_string()); + css_class_partials_response.insert(ThemeAttr::StrokeShadeNormal, "600".to_string()); + css_class_partials_response.insert(ThemeAttr::FillShadeNormal, "500".to_string()); + css_class_partials_response +} + +/// Returns [`CssClassPartials`] for the node for an [`ItemLocation`] of an +/// [`ItemInteractionWithin`]. +fn item_interaction_within_css_class_partials() -> CssClassPartials { + let mut css_class_partials = CssClassPartials::with_capacity(4); + css_class_partials.insert( + ThemeAttr::Animate, + "[stroke-dashoffset-move_1s_linear_infinite]".to_string(), + ); + css_class_partials.insert(ThemeAttr::ShapeColor, "blue".to_string()); + css_class_partials.insert(ThemeAttr::StrokeStyle, "dashed".to_string()); + css_class_partials +} + +/// Returns the node ID that ends with the calculated node ID, in case another +/// `Item` has provided an ancestor as context. +/// +/// Not sure if we need to find the longest node ID (which incurs one more +/// sort), but the current implementation just returns the first match. +fn node_id_with_ancestor_find( + node_id_to_item_locations: &IndexMap, + node_id_from: NodeId, +) -> NodeId { + node_id_to_item_locations + .keys() + .find(|node_id| node_id.ends_with(node_id_from.as_str())) + .cloned() + .unwrap_or(node_id_from) +} + +/// Returns a map of `NodeId` to the `ItemLocation` it is associated with, and +/// the `NodeHierarchy` constructed from the `ItemLocationTree`s. +fn node_id_mappings_and_hierarchy<'item_location>( + item_location_trees: &'item_location [ItemLocationTree], + item_location_count: usize, + #[cfg(feature = "output_progress")] item_location_to_item_id_sets: &'item_location HashMap< + ItemLocation, + HashSet, + >, +) -> NodeIdMappingsAndHierarchy<'item_location> { + let node_id_mappings_and_hierarchy = NodeIdMappingsAndHierarchy { + node_id_to_item_locations: IndexMap::with_capacity(item_location_count), + item_location_to_node_id_segments: HashMap::with_capacity(item_location_count), + node_hierarchy: NodeHierarchy::with_capacity(item_location_trees.len()), + #[cfg(feature = "output_progress")] + node_id_to_item_id_sets: HashMap::with_capacity(item_location_count), + }; + + item_location_trees.iter().fold( + node_id_mappings_and_hierarchy, + |mut node_id_mappings_and_hierarchy, item_location_tree| { + let NodeIdMappingsAndHierarchy { + node_id_to_item_locations, + item_location_to_node_id_segments, + node_hierarchy, + #[cfg(feature = "output_progress")] + node_id_to_item_id_sets, + } = &mut node_id_mappings_and_hierarchy; + + let item_location = item_location_tree.item_location(); + + // Probably won't go more than 8 deep. + let mut item_location_ancestors = SmallVec::<[&ItemLocation; 8]>::new(); + item_location_ancestors.push(item_location); + + let node_id = node_id_from_item_location(item_location_to_node_id_segments, || { + item_location_ancestors.clone().into_iter() + }); + + node_id_to_item_locations.insert(node_id.clone(), item_location); + + // Track the items that this node is associated with. + #[cfg(feature = "output_progress")] + { + let referrer_item_ids = item_location_to_item_id_sets.get(item_location); + if let Some(referrer_item_ids) = referrer_item_ids { + if let Some(node_referrer_item_ids) = node_id_to_item_id_sets.get_mut(&node_id) + { + node_referrer_item_ids.extend(referrer_item_ids); + } else { + let mut node_referrer_item_ids = + HashSet::with_capacity(referrer_item_ids.len()); + node_referrer_item_ids.extend(referrer_item_ids.iter()); + node_id_to_item_id_sets.insert(node_id.clone(), node_referrer_item_ids); + } + } + } + + let node_hierarchy_top_level = node_hierarchy_build_and_item_location_insert( + item_location_tree, + node_id_to_item_locations, + item_location_to_node_id_segments, + item_location_ancestors, + #[cfg(feature = "output_progress")] + item_location_to_item_id_sets, + #[cfg(feature = "output_progress")] + node_id_to_item_id_sets, + ); + node_hierarchy.insert(node_id, node_hierarchy_top_level); + + node_id_mappings_and_hierarchy + }, + ) +} + +/// Returns the [`NodeId`] for the given [`ItemLocation`]. +/// +/// This is computed from all of the node ID segments from all of the node's +/// ancestors. +fn node_id_from_item_location<'item_location, F, I>( + item_location_to_node_id_segments: &mut HashMap<&'item_location ItemLocation, String>, + item_location_ancestors_iter_fn: F, +) -> NodeId +where + F: Fn() -> I, + I: Iterator, +{ + let item_location_ancestors_iter_for_capacity = item_location_ancestors_iter_fn(); + let capacity = item_location_ancestors_iter_for_capacity.fold( + 0usize, + |capacity_acc, item_location_ancestor| { + let node_id_segment = item_location_to_node_id_segments + .entry(item_location_ancestor) + .or_insert_with(move || node_id_segment_from_item_location(item_location_ancestor)); + + capacity_acc + node_id_segment.len() + 3 + }, + ); + let mut node_id = item_location_ancestors_iter_fn() + .filter_map(|item_location_ancestor| { + item_location_to_node_id_segments.get(item_location_ancestor) + }) + .fold( + String::with_capacity(capacity), + |mut node_id_buffer, node_id_segment| { + node_id_buffer.push_str(node_id_segment); + node_id_buffer.push_str("___"); + node_id_buffer + }, + ); + + node_id.truncate(node_id.len() - "___".len()); + + NodeId::try_from(node_id).expect("Expected node ID from item location ID to be valid.") +} + +/// Returns a `&str` segment that can be used as part of the `NodeId` for the +/// given [`ItemLocation`]. +/// +/// An [`ItemLocation`]'s [`NodeId`] needs to be joined with the parent segments +/// from its ancestors, otherwise two different `path__path_to_file` +/// [`ItemLocation`]s may be accidentally merged. +fn node_id_segment_from_item_location(item_location: &ItemLocation) -> String { + let item_location_type = match item_location.r#type() { + ItemLocationType::Group => "group", + ItemLocationType::Host => "host", + ItemLocationType::Path => "path", + }; + let name = item_location.name(); + let name_transformed = + name.chars() + .fold(String::with_capacity(name.len()), |mut name_acc, c| { + match c { + 'a'..='z' | '0'..='9' => name_acc.push(c), + 'A'..='Z' => c.to_lowercase().for_each(|c| name_acc.push(c)), + _ => name_acc.push_str("__"), + } + name_acc + }); + + format!("{item_location_type}___{name_transformed}") +} + +/// Recursively constructs the `NodeHierarchy` and populates a map to facilitate +/// calculation of `InfoGraph` representing `ItemLocation`s. +/// +/// Each `Node` corresponds to one `ItemLocation`. +/// +/// Because: +/// +/// * Each `ItemInteraction` can include multiple `ItemLocation`s -- both nested +/// and separate, and +/// * We need to style each node +/// +/// it is useful to be able to retrieve the `ItemLocation` for each `Node` we +/// are adding attributes for. +fn node_hierarchy_build_and_item_location_insert<'item_location>( + item_location_tree: &'item_location ItemLocationTree, + node_id_to_item_locations: &mut IndexMap, + item_location_to_node_id_segments: &mut HashMap<&'item_location ItemLocation, String>, + item_location_ancestors: SmallVec<[&'item_location ItemLocation; 8]>, + #[cfg(feature = "output_progress")] item_location_to_item_id_sets: &'item_location HashMap< + ItemLocation, + HashSet, + >, + #[cfg(feature = "output_progress")] node_id_to_item_id_sets: &mut HashMap< + NodeId, + HashSet<&'item_location ItemId>, + >, +) -> NodeHierarchy { + let mut node_hierarchy = NodeHierarchy::with_capacity(item_location_tree.children().len()); + + item_location_tree + .children() + .iter() + .for_each(|child_item_location_tree| { + let child_item_location = child_item_location_tree.item_location(); + let mut child_item_location_ancestors = item_location_ancestors.clone(); + child_item_location_ancestors.push(child_item_location); + + let child_node_id = + node_id_from_item_location(item_location_to_node_id_segments, || { + child_item_location_ancestors.clone().into_iter() + }); + node_id_to_item_locations.insert(child_node_id.clone(), child_item_location); + + // Track the items that this node is associated with. + #[cfg(feature = "output_progress")] + { + let referrer_item_ids = item_location_to_item_id_sets.get(child_item_location); + if let Some(referrer_item_ids) = referrer_item_ids { + if let Some(node_referrer_item_ids) = + node_id_to_item_id_sets.get_mut(&child_node_id) + { + node_referrer_item_ids.extend(referrer_item_ids); + } else { + let mut node_referrer_item_ids = + HashSet::with_capacity(referrer_item_ids.len()); + node_referrer_item_ids.extend(referrer_item_ids.iter()); + node_id_to_item_id_sets + .insert(child_node_id.clone(), node_referrer_item_ids); + } + } + } + + let child_hierarchy = node_hierarchy_build_and_item_location_insert( + child_item_location_tree, + node_id_to_item_locations, + item_location_to_node_id_segments, + child_item_location_ancestors, + #[cfg(feature = "output_progress")] + item_location_to_item_id_sets, + #[cfg(feature = "output_progress")] + node_id_to_item_id_sets, + ); + node_hierarchy.insert(child_node_id, child_hierarchy); + }); + + node_hierarchy +} + +struct NodeIdMappingsAndHierarchy<'item_location> { + node_id_to_item_locations: IndexMap, + item_location_to_node_id_segments: HashMap<&'item_location ItemLocation, String>, + node_hierarchy: NodeHierarchy, + /// Mapping to Item IDs that interact with the `ItemLocation` that the + /// `NodeId` represents. + #[cfg(feature = "output_progress")] + node_id_to_item_id_sets: HashMap>, +} + +struct ItemInteractionsProcessCtx<'f, 'item_location> { + outcome_info_graph_variant: &'f OutcomeInfoGraphVariant, + item_count: usize, + item_location_count: usize, + item_to_item_interactions: &'item_location IndexMap>, + node_id_to_item_locations: &'f IndexMap, + item_location_to_node_id_segments: &'f mut HashMap<&'item_location ItemLocation, String>, +} + +struct ItemInteractionsProcessingCtxExample<'f, 'item_location> { + node_id_to_item_locations: &'f IndexMap, + item_location_to_node_id_segments: &'f mut HashMap<&'item_location ItemLocation, String>, + edges: &'f mut Edges, + tag_items: &'f mut TagItems, + tag_id: &'f TagId, + tag_styles_focus: &'f mut TagStyles, +} + +struct ItemInteractionsProcessingCtxCurrent<'f, 'item_location> { + node_id_to_item_locations: &'f IndexMap, + item_location_to_node_id_segments: &'f mut HashMap<&'item_location ItemLocation, String>, + edges: &'f mut Edges, + theme: &'f mut Theme, + #[cfg(feature = "output_progress")] + progress_status: ProgressStatus, +} + +struct ItemInteractionsProcessedExample { + edges: Edges, + graphviz_attrs: GraphvizAttrs, + tag_items: TagItems, + tag_styles_focus: TagStyles, +} + +struct ItemInteractionsProcessedCurrent<'item_state> { + edges: Edges, + graphviz_attrs: GraphvizAttrs, + theme: Theme, + /// Progress of each item. + #[cfg(feature = "output_progress")] + item_progress_statuses: &'item_state HashMap, + marker: PhantomData<&'item_state ()>, +} + +struct ItemInteractionsProcessed { + edges: Edges, + graphviz_attrs: GraphvizAttrs, + theme: Theme, + tag_items: Option, + tag_styles_focus: Option, +} diff --git a/crate/webi_output/src/progress_info_graph_calculator.rs b/crate/webi_output/src/progress_info_graph_calculator.rs new file mode 100644 index 000000000..589194ebb --- /dev/null +++ b/crate/webi_output/src/progress_info_graph_calculator.rs @@ -0,0 +1,23 @@ +use std::collections::HashMap; + +use dot_ix_model::info_graph::InfoGraph; +use peace_core::{progress::ProgressStatus, ItemId}; +use peace_rt_model::Flow; + +/// Calculates the actual `InfoGraph` for a flow's progress. +#[derive(Debug)] +pub struct ProgressInfoGraphCalculator; + +impl ProgressInfoGraphCalculator { + /// Returns the calculated `InfoGraph`. + pub fn calculate( + flow: &Flow, + item_progress_statuses: &HashMap, + ) -> InfoGraph + where + E: 'static, + { + let flow_spec_info = flow.flow_spec_info(); + flow_spec_info.to_progress_info_graph_with_statuses(item_progress_statuses) + } +} diff --git a/crate/webi_output/src/webi_output.rs b/crate/webi_output/src/webi_output.rs index f9ca03a50..a76818788 100644 --- a/crate/webi_output/src/webi_output.rs +++ b/crate/webi_output/src/webi_output.rs @@ -1,23 +1,24 @@ -use std::{fmt::Debug, net::SocketAddr}; - -use peace_flow_model::FlowSpecInfo; use peace_fmt::Presentable; use peace_rt_model_core::{async_trait, output::OutputWrite}; use peace_value_traits::AppError; -use peace_webi_model::WebiError; - -use crate::WebiServer; +use peace_webi_model::WebUiUpdate; +use tokio::sync::mpsc; cfg_if::cfg_if! { if #[cfg(feature = "output_progress")] { - use peace_core::progress::{ - // ProgressComplete, - // ProgressLimit, - // ProgressStatus, - ProgressTracker, - // ProgressUpdate, - ProgressUpdateAndId, + use peace_core::{ + progress::{ + CmdBlockItemInteractionType, + // ProgressComplete, + // ProgressLimit, + // ProgressStatus, + ProgressTracker, + // ProgressUpdate, + ProgressUpdateAndId, + }, + ItemId, }; + use peace_item_model::ItemLocationState; use peace_rt_model_core::CmdProgressTracker; } } @@ -25,29 +26,28 @@ cfg_if::cfg_if! { /// An `OutputWrite` implementation that writes to web elements. #[derive(Clone, Debug)] pub struct WebiOutput { - /// IP address and port to listen on. - socket_addr: Option, - /// Flow to display to the user. - flow_spec_info: FlowSpecInfo, + /// Channel to notify the `CmdExecution` task / `leptos` to update the UI. + /// + /// This can be: + /// + /// * Progress `InfoGraph` diagram needs to be restyled. + /// * Outcome `InfoGraph` diagram needs to be restyled. + /// * Execution result to show to the user. + web_ui_update_tx: Option>, } impl WebiOutput { - pub fn new(socket_addr: Option, flow_spec_info: FlowSpecInfo) -> Self { + /// Returns a new `WebiOutput`. + pub fn new(web_ui_update_tx: mpsc::Sender) -> Self { Self { - socket_addr, - flow_spec_info, + web_ui_update_tx: Some(web_ui_update_tx), } } -} -impl WebiOutput { - pub async fn start(&self) -> Result<(), WebiError> { - let Self { - socket_addr, - flow_spec_info, - } = self.clone(); - - WebiServer::new(socket_addr, flow_spec_info).start().await + pub fn clone_without_tx(&self) -> Self { + Self { + web_ui_update_tx: None, + } } } @@ -59,12 +59,57 @@ where #[cfg(feature = "output_progress")] async fn progress_begin(&mut self, _cmd_progress_tracker: &CmdProgressTracker) {} + #[cfg(feature = "output_progress")] + async fn cmd_block_start( + &mut self, + cmd_block_item_interaction_type: CmdBlockItemInteractionType, + ) { + if let Some(web_ui_update_tx) = self.web_ui_update_tx.as_ref() { + let _result = web_ui_update_tx + .send(WebUiUpdate::CmdBlockStart { + cmd_block_item_interaction_type, + }) + .await; + } + } + + #[cfg(feature = "output_progress")] + async fn item_location_state( + &mut self, + item_id: ItemId, + item_location_state: ItemLocationState, + ) { + if let Some(web_ui_update_tx) = self.web_ui_update_tx.as_ref() { + let _result = web_ui_update_tx + .send(WebUiUpdate::ItemLocationState { + item_id, + item_location_state, + }) + .await; + } + } + #[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, ) { + let item_id = progress_update_and_id.item_id.clone(); + let progress_status = progress_tracker.progress_status().clone(); + let progress_limit = progress_tracker.progress_limit(); + let message = progress_tracker.message().cloned(); + + if let Some(web_ui_update_tx) = self.web_ui_update_tx.as_ref() { + let _result = web_ui_update_tx + .send(WebUiUpdate::ItemProgressStatus { + item_id, + progress_status, + progress_limit, + message, + }) + .await; + } } #[cfg(feature = "output_progress")] @@ -75,7 +120,14 @@ where AppErrorT: std::error::Error, P: Presentable, { - todo!() + // TODO: send rendered / renderable markdown to the channel. + let markdown_src = String::from("TODO: presentable.present(md_presenter)."); + if let Some(web_ui_update_tx) = self.web_ui_update_tx.as_ref() { + let _result = web_ui_update_tx + .send(WebUiUpdate::Markdown { markdown_src }) + .await; + } + Ok(()) } async fn write_err(&mut self, _error: &AppErrorT) -> Result<(), AppErrorT> diff --git a/crate/webi_output/src/webi_server.rs b/crate/webi_output/src/webi_server.rs index 0003c6e7a..d8530310e 100644 --- a/crate/webi_output/src/webi_server.rs +++ b/crate/webi_output/src/webi_server.rs @@ -1,45 +1,388 @@ -use std::{fmt::Debug, net::SocketAddr, path::Path}; +use std::{net::SocketAddr, path::Path}; use axum::Router; use futures::stream::{self, StreamExt, TryStreamExt}; use leptos::view; use leptos_axum::LeptosRoutes; -use peace_flow_model::FlowSpecInfo; -use peace_webi_components::Home; -use peace_webi_model::WebiError; -use tokio::io::AsyncWriteExt; +use peace_cmd_model::CmdExecutionId; +use peace_core::FlowId; +use peace_webi_components::{ChildrenFn, Home}; +use peace_webi_model::{WebUiUpdate, WebiError}; +use tokio::{io::AsyncWriteExt, sync::mpsc}; use tower_http::services::ServeDir; -/// An `OutputWrite` implementation that writes to web elements. -#[derive(Clone, Debug)] -pub struct WebiServer { - /// IP address and port to listen on. - socket_addr: Option, - /// Flow to display to the user. - flow_spec_info: FlowSpecInfo, -} +use crate::{CmdExecSpawnCtx, CmdExecToLeptosCtx, FlowWebiFns, WebiOutput}; + +#[cfg(feature = "item_interactions")] +use crate::OutcomeInfoGraphCalculator; +#[cfg(feature = "item_interactions")] +use peace_webi_model::OutcomeInfoGraphVariant; + +#[cfg(feature = "output_progress")] +use std::collections::HashMap; + +#[cfg(feature = "output_progress")] +use peace_core::progress::CmdBlockItemInteractionType; + +#[cfg(feature = "output_progress")] +use crate::ProgressInfoGraphCalculator; + +/// Maximum number of `CmdExecReqT`s to queue up. +const CMD_EXEC_REQUEST_CHANNEL_LIMIT: usize = 1024; + +/// Web server that runs the following work: +/// +/// * UI rendering with `leptos`. +/// * `CmdExecution` through receiving requests from leptos. +/// * Updating `leptos` context data for components to render. +#[derive(Debug)] +pub struct WebiServer; impl WebiServer { - pub fn new(socket_addr: Option, flow_spec_info: FlowSpecInfo) -> Self { - Self { + /// Starts the web server. + /// + /// ## Parameters + /// + /// * `socker_addr`: IP address and port to listen on. + pub async fn start( + socket_addr: Option, + app_home: ChildrenFn, + flow_webi_fns: FlowWebiFns, + ) -> Result<(), WebiError> + where + E: 'static, + CmdExecReqT: Send + 'static, + { + let cmd_exec_to_leptos_ctx = CmdExecToLeptosCtx::default(); + let (cmd_exec_request_tx, cmd_exec_request_rx) = + mpsc::channel::(CMD_EXEC_REQUEST_CHANNEL_LIMIT); + + let flow_id = flow_webi_fns.flow.flow_id().clone(); + let webi_server_task = Self::leptos_server_start( socket_addr, - flow_spec_info, - } + app_home, + cmd_exec_request_tx, + cmd_exec_to_leptos_ctx.clone(), + flow_id, + ); + let cmd_execution_listener_task = Self::cmd_execution_listener( + cmd_exec_request_rx, + cmd_exec_to_leptos_ctx, + flow_webi_fns, + ); + + tokio::try_join!(webi_server_task, cmd_execution_listener_task).map(|((), ())| ()) } -} -impl WebiServer { - pub async fn start(&mut self) -> Result<(), WebiError> { - let Self { - socket_addr, - flow_spec_info, - } = self; + async fn cmd_execution_listener( + mut cmd_exec_request_rx: mpsc::Receiver, + cmd_exec_to_leptos_ctx: CmdExecToLeptosCtx, + flow_webi_fns: FlowWebiFns, + ) -> Result<(), WebiError> + where + E: 'static, + CmdExecReqT: Send + 'static, + { + // TODO: + // + // 1. Listen for params specs + // 2. Instantiate `CmdCtx` + // 3. Calculate example `info_graph`s + // 4. Insert into `FlowInfoGraphs`. + let FlowWebiFns { + flow, + outcome_info_graph_fn, + cmd_exec_spawn_fn, + } = flow_webi_fns; + let outcome_info_graph_fn = &outcome_info_graph_fn; + #[cfg(feature = "output_progress")] + let item_count = flow.graph().node_count(); + + let CmdExecToLeptosCtx { + flow_progress_example_info_graphs, + flow_progress_actual_info_graphs, + flow_outcome_example_info_graphs, + flow_outcome_actual_info_graphs, + mut cmd_exec_interrupt_txs, + cmd_execution_id: cmd_execution_id_arc, + } = cmd_exec_to_leptos_ctx; + + // TODO: remove this mock? + // Should we have one WebiOutput for the whole server? doesn't seem right. + let (web_ui_update_tx, _web_ui_update_rx) = mpsc::channel(128); + let mut webi_output_mock = WebiOutput::new(web_ui_update_tx); + let flow_spec_info = flow.flow_spec_info(); + let flow_progress_example_info_graph = flow_spec_info.to_progress_info_graph(); + let flow_outcome_example_info_graph = outcome_info_graph_fn( + &mut webi_output_mock, + Box::new(|flow, params_specs, resources| { + #[cfg(all(feature = "item_interactions", feature = "item_state_example"))] + { + OutcomeInfoGraphCalculator::calculate::( + flow, + params_specs, + resources, + OutcomeInfoGraphVariant::Example, + ) + } + + #[cfg(not(all(feature = "item_interactions", feature = "item_state_example")))] + { + use dot_ix_model::info_graph::InfoGraph; + + let _flow = flow; + let _params_specs = params_specs; + let _resources = resources; + + InfoGraph::default() + } + }), + ) + .await; + + let flow_id = flow.flow_id(); + if let Ok(mut flow_progress_example_info_graphs) = flow_progress_example_info_graphs.lock() + { + flow_progress_example_info_graphs + .insert(flow_id.clone(), flow_progress_example_info_graph); + } + if let Ok(mut flow_outcome_example_info_graphs) = flow_outcome_example_info_graphs.lock() { + flow_outcome_example_info_graphs + .insert(flow_id.clone(), flow_outcome_example_info_graph); + } + + let (cmd_exec_join_handle_tx, mut cmd_exec_join_handle_rx) = mpsc::channel(128); + + let cmd_execution_starter_task = async move { + let mut cmd_execution_id_next = CmdExecutionId::new(0u64); + while let Some(cmd_exec_request) = cmd_exec_request_rx.recv().await { + // Note: If we don't have a large enough buffer, we might drop updates, + // which may mean a node appears to still be in progress when it has completed. + let (web_ui_update_tx, web_ui_update_rx) = mpsc::channel(1024); + let webi_output = WebiOutput::new(web_ui_update_tx); + + let webi_output_clone = webi_output.clone_without_tx(); + let CmdExecSpawnCtx { + interrupt_tx, + cmd_exec_task, + } = cmd_exec_spawn_fn(webi_output, cmd_exec_request); + + let cmd_execution_id = cmd_execution_id_next; + cmd_execution_id_next = CmdExecutionId::new(*cmd_execution_id + 1); + + cmd_exec_join_handle_tx + .send((cmd_execution_id, webi_output_clone, web_ui_update_rx)) + .await + .expect("Expected `cmd_execution_receiver_task` to be running."); + if let Some(interrupt_tx) = interrupt_tx { + cmd_exec_interrupt_txs.insert(cmd_execution_id, interrupt_tx); + } + + let local_set = tokio::task::LocalSet::new(); + local_set + .run_until(async move { + let cmd_exec_join_handle = tokio::task::spawn_local(cmd_exec_task); + + match cmd_exec_join_handle.await { + Ok(()) => { + eprintln!("`cmd_execution` completed.") + } + Err(join_error) => { + eprintln!( + "Failed to wait for `cmd_execution` to complete. {join_error}" + ); + // TODO: insert CmdExecution failed status + } + } + }) + .await; + } + }; + + let cmd_execution_receiver_task = async move { + while let Some((cmd_execution_id, mut webi_output, mut web_ui_update_rx)) = + cmd_exec_join_handle_rx.recv().await + { + if let Ok(mut cmd_execution_id_guard) = cmd_execution_id_arc.lock() { + *cmd_execution_id_guard = Some(cmd_execution_id); + } else { + eprintln!("Unable to insert cmd_execution_id to run: {cmd_execution_id:?}"); + } + + let flow_progress_actual_info_graphs = flow_progress_actual_info_graphs.clone(); + let flow_outcome_actual_info_graphs = flow_outcome_actual_info_graphs.clone(); + + #[cfg(not(feature = "output_progress"))] + let flow_spec_info = flow_spec_info.clone(); + #[cfg(feature = "output_progress")] + let flow_ref = &flow; + + // Update `InfoGraph`s every time `progress_update` is sent. + let web_ui_update_task = async move { + // Keep track of item execution progress. + #[cfg(feature = "output_progress")] + let mut cmd_block_item_interaction_type = CmdBlockItemInteractionType::Local; + #[cfg(feature = "output_progress")] + let mut item_location_states = HashMap::with_capacity(item_count); + #[cfg(feature = "output_progress")] + let mut item_progress_statuses = HashMap::with_capacity(item_count); + + while let Some(web_ui_update) = web_ui_update_rx.recv().await { + match web_ui_update { + #[cfg(feature = "output_progress")] + WebUiUpdate::CmdBlockStart { + cmd_block_item_interaction_type: + cmd_block_item_interaction_type_next, + } => { + cmd_block_item_interaction_type = + cmd_block_item_interaction_type_next; + } + #[cfg(feature = "output_progress")] + WebUiUpdate::ItemLocationState { + item_id, + item_location_state, + } => { + item_location_states.insert(item_id, item_location_state); + } + #[cfg(feature = "output_progress")] + WebUiUpdate::ItemProgressStatus { + item_id, + progress_status, + progress_limit: _, + message: _, + } => { + item_progress_statuses.insert(item_id, progress_status); + } + WebUiUpdate::Markdown { markdown_src: _ } => { + // TODO: render markdown on server side? + } + } + + #[cfg(not(feature = "output_progress"))] + let flow_progress_actual_info_graph = + flow_spec_info.to_progress_info_graph(); + + #[cfg(feature = "output_progress")] + let flow_progress_actual_info_graph = + ProgressInfoGraphCalculator::calculate( + flow_ref, + &item_progress_statuses, + ); + + if let Ok(mut flow_progress_actual_info_graphs) = + flow_progress_actual_info_graphs.lock() + { + flow_progress_actual_info_graphs + .insert(cmd_execution_id, flow_progress_actual_info_graph); + } + + #[cfg(feature = "output_progress")] + let item_location_states_snapshot = item_location_states.clone(); + #[cfg(feature = "output_progress")] + let item_progress_statuses_snapshot = item_progress_statuses.clone(); + + let flow_outcome_actual_info_graph = outcome_info_graph_fn( + &mut webi_output, + Box::new(move |flow, params_specs, resources| { + #[cfg(feature = "output_progress")] + let item_location_states = item_location_states_snapshot.clone(); + #[cfg(feature = "output_progress")] + let item_progress_statuses = + item_progress_statuses_snapshot.clone(); + + #[cfg(feature = "item_interactions")] + { + OutcomeInfoGraphCalculator::calculate::( + flow, + params_specs, + resources, + OutcomeInfoGraphVariant::Current { + #[cfg(feature = "output_progress")] + cmd_block_item_interaction_type, + #[cfg(feature = "output_progress")] + item_location_states, + #[cfg(feature = "output_progress")] + item_progress_statuses, + }, + ) + } + + #[cfg(not(feature = "item_interactions"))] + { + use dot_ix_model::info_graph::InfoGraph; + + let _flow = flow; + let _params_specs = params_specs; + let _resources = resources; + + InfoGraph::default() + } + }), + ) + .await; + + if let Ok(mut flow_outcome_actual_info_graphs) = + flow_outcome_actual_info_graphs.lock() + { + flow_outcome_actual_info_graphs + .insert(cmd_execution_id, flow_outcome_actual_info_graph); + } + } + }; + + // ```rust,ignore + // let cmd_exec_join_task = async move { + // match cmd_exec_join_handle.await { + // Ok(()) => {} + // Err(join_error) => { + // eprintln!( + // "Failed to wait for `cmd_execution` to complete. {join_error}" + // ); + // // TODO: insert CmdExecution failed status + // } + // } + // }; + // ``` + + // tokio::join!(web_ui_update_task, cmd_exec_join_task); + + // TODO: spawn task and go back to waiting, instead of waiting for this task, or + // drop the txes + web_ui_update_task.await; + } + }; + + tokio::join!(cmd_execution_starter_task, cmd_execution_receiver_task); + + Ok(()) + } + + /// + /// # Parameters + /// + /// * `socket_addr`: IP address and port to listen on. + async fn leptos_server_start( + socket_addr: Option, + app_home: ChildrenFn, + cmd_exec_request_tx: mpsc::Sender, + cmd_exec_to_leptos_ctx: CmdExecToLeptosCtx, + flow_id: FlowId, + ) -> Result<(), WebiError> + where + CmdExecReqT: Send + 'static, + { // 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(move || view! { }); + let routes = leptos_axum::generate_route_list({ + let app_home = app_home.clone(); + move || { + let app_home = app_home.clone(); + view! { } + } + }); stream::iter(crate::assets::ASSETS.iter()) .map(Result::<_, WebiError>::Ok) @@ -65,7 +408,6 @@ impl WebiServer { }) .await?; - let flow_spec_info = flow_spec_info.clone(); let router = Router::new() // serve the pkg directory .nest_service( @@ -78,8 +420,33 @@ impl WebiServer { .leptos_routes_with_context( &leptos_options, routes, - move || leptos::provide_context(flow_spec_info.clone()), - move || view! { }, + move || { + // Add global state here if necessary + let CmdExecToLeptosCtx { + flow_progress_example_info_graphs, + flow_progress_actual_info_graphs, + flow_outcome_example_info_graphs, + flow_outcome_actual_info_graphs, + cmd_exec_interrupt_txs, + cmd_execution_id, + } = cmd_exec_to_leptos_ctx.clone(); + + let (flow_id, flow_id_set) = leptos::create_signal(flow_id.clone()); + + leptos::provide_context(flow_id); + leptos::provide_context(flow_id_set); + leptos::provide_context(flow_progress_example_info_graphs.clone()); + leptos::provide_context(flow_progress_actual_info_graphs.clone()); + leptos::provide_context(flow_outcome_example_info_graphs.clone()); + leptos::provide_context(flow_outcome_actual_info_graphs.clone()); + leptos::provide_context(cmd_exec_interrupt_txs.clone()); + leptos::provide_context(cmd_execution_id.clone()); + leptos::provide_context(cmd_exec_request_tx.clone()); + }, + move || { + let app_home = app_home.clone(); + view! { } + }, ) .with_state(leptos_options); diff --git a/deny.toml b/deny.toml index 501f94a2b..74ac69fc3 100644 --- a/deny.toml +++ b/deny.toml @@ -11,6 +11,7 @@ # Root options +[graph] # If 1 or more target triples (and optionally, target_features) are specified, # only the specified targets will be checked when running `cargo deny check`. # This means, if a particular package is only ever used as a target specific @@ -46,6 +47,8 @@ no-default-features = false # If set, these feature will be enabled when collecting metadata. If `--features` # is specified on the cmd line they will take precedence over this option. features = ["error_reporting", "output_progress"] + +[output] # When outputting inclusion graphs in diagnostics that include features, this # option can be used to specify the depth at which feature edges will be added. # This option is included since the graphs can be quite large and the addition @@ -57,34 +60,22 @@ feature-depth = 1 # More documentation for the advisories section can be found here: # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html [advisories] +version = 2 # The path where the advisory database is cloned/fetched into db-path = "~/.cargo/advisory-db" # The url(s) of the advisory databases to use db-urls = ["https://github.com/rustsec/advisory-db"] -# The lint level for security vulnerabilities -vulnerability = "deny" -# The lint level for unmaintained crates -unmaintained = "warn" # The lint level for crates that have been yanked from their source registry yanked = "warn" -# The lint level for crates with security notices. Note that as of -# 2019-12-17 there are no security notice advisories in -# https://github.com/rustsec/advisory-db -notice = "warn" # A list of advisory IDs to ignore. Note that ignored advisories will still # output a note when they are encountered. ignore = [ - #"RUSTSEC-0000-0000", + # `proc-macro-error` is Unmaintained. + # + # Transitive dependency of `syn_derive`. + # Pending https://github.com/Kyuuhachi/syn_derive/issues/4. + "RUSTSEC-2024-0370", ] -# Threshold for security vulnerabilities, any vulnerability with a CVSS score -# lower than the range specified will be ignored. Note that ignored advisories -# will still output a note when they are encountered. -# * None - CVSS Score 0.0 -# * Low - CVSS Score 0.1 - 3.9 -# * Medium - CVSS Score 4.0 - 6.9 -# * High - CVSS Score 7.0 - 8.9 -# * Critical - CVSS Score 9.0 - 10.0 -#severity-threshold = # If this is true, then cargo deny will use the git executable to fetch advisory database. # If this is false, then it uses a built-in git library. @@ -96,8 +87,7 @@ ignore = [ # More documentation for the licenses section can be found here: # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html [licenses] -# The lint level for crates which do not have a detectable license -unlicensed = "deny" +version = 2 # List of explicitly allowed licenses # See https://spdx.org/licenses/ for list of possible licenses # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. @@ -115,26 +105,6 @@ allow = [ "OpenSSL", "Zlib", ] -# List of explicitly disallowed licenses -# See https://spdx.org/licenses/ for list of possible licenses -# [possible values: any SPDX 3.11 short identifier (+ optional exception)]. -deny = [ - #"Nokia", -] -# Lint level for licenses considered copyleft -copyleft = "deny" -# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses -# * both - The license will be approved if it is both OSI-approved *AND* FSF -# * either - The license will be approved if it is either OSI-approved *OR* FSF -# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF -# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved -# * neither - This predicate is ignored and the default lint level is used -allow-osi-fsf-free = "neither" -# Lint level used when no other predicates are matched -# 1. License isn't in the allow or deny lists -# 2. License isn't copyleft -# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither" -default = "deny" # The confidence threshold for detecting a license from license text. # The higher the value, the more closely the license text must be to the # canonical license text of a valid SPDX license file. diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index 7f44e1f03..295e7e72b 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -39,6 +39,9 @@ - [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) + - [API Design](technical_concepts/diagrams/outcome/api_design.md) + - [Interaction Merging](technical_concepts/diagrams/outcome/interaction_merging.md) + - [Aesthetics and Clarity](technical_concepts/diagrams/outcome/aesthetics_and_clarity.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) diff --git a/doc/src/technical_concepts/diagrams/outcome.md b/doc/src/technical_concepts/diagrams/outcome.md index 3ddead810..4179e0c9c 100644 --- a/doc/src/technical_concepts/diagrams/outcome.md +++ b/doc/src/technical_concepts/diagrams/outcome.md @@ -141,7 +141,7 @@ impl Item for FileDownload { let mut item_locations = Vec::new(); if let Some(host) = host { - item_locations.push(ItemLocation::Server { host, port }); + item_locations.push(ItemLocation::Host { host, port }); if let Some(url) = url { // May be rendered using the last segment of the URL as the node name. @@ -248,25 +248,27 @@ Cloud provider name, region, availability zone, etc. ```rust ,ignore #[derive(Debug)] enum ItemLocation { - Server(ItemLocationServer), + Host(ItemLocationHost), + Url(Url), } - struct ItemLocationServer { + struct ItemLocationHost { host: Host, port: Option, } impl ItemLocation { fn from_url(url: &Url) -> Self { - Self::Url(ItemLocationUrl::from(url)) + Self::Url(url.clone()) } } - impl From<&Url> for ItemLocationServer { + impl From<&Url> for ItemLocationHost { fn from(url: &Url) -> Self { let host = url .map(Url::host) - .map(Host::to_owned); + .map(Host::to_owned) + .expect("Expected URL to contain a host."); let port = url .map(Url::port_or_known_default); @@ -333,7 +335,7 @@ Cloud provider name, region, availability zone, etc. ) -> ItemLocation { match level { - MyItemLocationLevel::Server => { + MyItemLocationLevel::Host => { let host = params_partial .src() .map(Url::host) @@ -341,7 +343,7 @@ Cloud provider name, region, availability zone, etc. let port = params_partial .src() .map(Url::port_or_known_default); - ItemLocation::Server { host, port } + ItemLocation::Host { host, port } } _ => todo!(), } @@ -459,7 +461,7 @@ It can be confusing to follow if these keep changing, which is counter productiv May mean every `ItemLocation` that is unknown, is still populated: ```rust ,ignore - let item_location_server = item_location_server.unwrap_or(ItemLocation::ServerUnknown); + let item_location_server = item_location_server.unwrap_or(ItemLocation::HostUnknown); let item_location_url = item_location_url.unwrap_or(ItemLocation::UrlUnknown); vec![ @@ -488,6 +490,17 @@ Conceptually, `Item`s can be thought of either an edge or a node: * **Node:** The item represents the destination thing. +### Use Cases + +1. Upload a file -- one source, one dest. +2. Download a file -- one source, one dest. +3. Launch servers -- one source (localhost), one dest (AWS). +4. Wait for servers to start up -- multiple within (do we need the `ItemLocationTree` for the cloud provider / subnet context? or leverage previous resource tracking to work it out?). +5. Wait for endpoints to become available -- one source, multiple dest (query each endpoint). +6. Do we want `ItemInteraction`s to be queried multiple times while `Apply` is happening? -- i.e. some servers may have started up, and so we need the `Item` to report that to us. +7. Notably, we want these `ItemInteraction`s to be queryable without the items actually existing -- so we can generate diagrams to demonstrate what *would* happen upon execution. + + ### 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: @@ -514,7 +527,7 @@ If we could show: 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) +[dot_ix](https://azriel.im/dot_ix/#src=LQhQBMEsCcFMGMAukD2A7AXAAgG62svAIYA2oAFpPkdPOQJ4ahZYDmki5ArgEZMst2nXgH0iAB3EiAXpHHYA3gF9QzLCRTES5FAGdE-ARKngUAdzQai4RSoFZjI2AA9E0IkluqWRM7sMskEQAtiLiKCSQ8IxYymqBISLQEbBe9pBo+kRo8LBhyQBmkCSpsXYCugDMIjxc8ADWsAbxFdUoPABWCAZlqmgo4HloIbD+ahpaOvrY9gIARBOkU4hzar7+s5vz66uCHNx8W-ZzQge7bPuijrLy25IAdDfnjqYWVjazcy-mlijWz5InK53J5Nl9AS43B4VmogqFwpFojNPnCwhEovRzqjkiVkWDsSksZlENlcvkUEVcVg5hksjk8uJCsVYOcqjU6o0els5mzag0mqy2p1unjjmz2l0kKtQP1BiJBrp4GMWIttHouWCAJooLjQLDwFDBcRcRD4Z5+UVHakAQWCRGk6CwAHVYDwsABlfA4KKjc6nXiWz4AcUuPHOAGILsIeGJATdkQByAB+CbWgNev2sovB4kecgBUkhII18wA9ChxIhS8YsYkERjA3MAKIAYQATNhrSQNGYPZUsAAlWD-WGJHGlI7N9tYACS1oAslh69EHIgSXRgrA0DDAsTSQymVSp22sLSSfSsOPV+vyJvt4L2fyS8dBsEUMAaMgCtDdA+JSLJ2MPNxGlWU8lgN8OkgZV1E0JZ1UDLBAF4NwBunbWC0rVmQBAMkAeD+1H9Q5MKQwBMHfw0NYykeMBCQwBBnbTEwfneUUkMAU536KBKFQVmJDACGd0d4XRJEtiQwBdnf4pIUkQ5DAAZd2E93pclKVKQAeDcAd-28JYXkOSaRDAD4NwBivbUcVhS4zZaNUWBwFYUZDG+N4-nAEQKI44tsAAbTszNwAAGgcCFgWhABdcTl3oJyCSpNzUVC3yItgYKElCcdwvkslGQpZl3Li2LUoPDKSgSvyGPs6wnOMyUeg89NGIc3zyu6QrosEsKRHq0EorrZq6qFCrgrUSMSCIegdUQMjo2cm4nMcItoXcgiJrkXzpoCpA+tYdxxHIb1pDENdoBgyzrJEYIMhKNB5Vgb8uBIHpKlUTgINSNRN2gI6zH2GoiF0Uo3C4WAjMQegShglgwIuq6bpBio3BQRoRHe8BOGwBMAEZU02GbkfgXVdBQaBgHCDJTT1ZIuDQQZwGAEhWHRgRDryQYIcQKGWExrBKHAQY0BaHgSD+sQ0EgO1TQ+AAyXn+eyIWiBFlpNPICQ8gNDRoGwCX-s2fRkjh-QgdKcAvvISyWEjXQdAIBXyawAo8awNzRYAPnEGXyECjA3K12HYGAA2zZodxGEqQr7E9uGEaRrAEzcttxGcQLaZDmGdYVuV+mgO0SGRgA2AAGHOE4ESkSFalOhjxjPkYAVjzgvWYC7AHeAOWBHtp2XbdqXhe9j2k+932dAKApvsQYA3zwEQABZdBESI0GHaARAyIpBdNYPZmx-a8YJlAifwFpPPeMqepFZuAB5T+wAAqdWBel2X7EiVhyB6UXH+f5ui5L6wy-T0hkZRmuH9ijFz9nKHQeBVaR2rrXa2wCv5yhttjDYqNAGbE-qAvI0JIB4H-qg2Yoc8gYJEGnCukcJ54MTtrQhpcRDgPwMjSoFDoZUPgZgpA2DSgJmrvnZuBDWqAypKYNcllm7O05hkVgDCC7rACAIc+V836jU1grcQSsIh42wCEHge9NgjVnmolW2B6CwG7OYFoqpliyJYPIrAl9FG8JUQYjRWB1YtAIlYrANi7GQCfko-BjiRDK2cetIaLQtJPg8egmhJC-6RzbEwlgUTv60JQBA3BPC0FwKIYgrgyD4kZNmEkuUWCcFxISVgPhRCYmZygeUypNC6GQITOQgplCvasLEOw0pCZc6tOYe03WgiUDCPAKI6wUA0CSMjpUGBftVGBPUZArRe9QBKg2I3NQAABRo9ACjuE3LoCpvcfaGwpEPJoo9UmwFiC0HOABSAQCgjlUJOWbM5w9sATwABwAG4sDlBYAA+5sRnle1eQPc5PQc5-PKOUbZsBdn7NGKCxo4L3kXLHt7OAEDvo3PsMClgTyCFosHh8rA0L-ktCBQ8olxz+7op6N8mFagVBAA) @@ -522,7 +535,7 @@ 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. +3. It is useful to show animated edges while that step is in progress, and hide them after.
Old idea for code 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 index d9afd5128..4f09bec62 100644 --- 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 @@ -1,148 +1,145 @@ - - + - + G - + cluster_aws - -☁️ -aws -Amazon Web -Services + +☁️ +aws +Amazon Web Services - + cluster_s3_bucket - -🪣 -s3_bucket -demo-artifacts + +🪣 +s3_bucket +demo-artifacts - + cluster_localhost - -💻 -localhost -Your -computer + +💻 +localhost +Your computer - + cluster_github - -🐙 -github -Github + +🐙 +github +Github - + iam_policy - -📝 -iam_policy -EC2: -Allow -S3 Read + +📝 +iam_policy +EC2: Allow S3 Read - + iam_role - -🔰 -iam_role -EC2 IAM -policy -attachment + +🔰 +iam_role +EC2 IAM policy attachment - diff --git a/doc/src/technical_concepts/diagrams/outcome/aesthetics_and_clarity.md b/doc/src/technical_concepts/diagrams/outcome/aesthetics_and_clarity.md new file mode 100644 index 000000000..23cf33f47 --- /dev/null +++ b/doc/src/technical_concepts/diagrams/outcome/aesthetics_and_clarity.md @@ -0,0 +1,45 @@ +# Aesthetics and Clarity + +Aesthetics helps reduce the "ick" in a diagram, reducing the impedance that a user experiences when viewing a diagram. Examples of icks are: + +1. Visual clutter. +2. A node's dimensions being significantly different compared to other nodes. +3. Oversaturated colour. +4. Colours that are excessively solid for a less relevant concept. + +Clarity is how well a user understands the information that a diagram is presenting. Examples of adding clarity are: + +1. (Understandable) visual cues such as emojis in place of text. +2. Reducing detail to what is most relevant. + +Consider the last diagram from [Interaction Merging](interaction_merging.md#5-animate-and-show-edges-iteminteractions-for-each-step): + + +
+source + +The following things that could make the diagram more digestable: + +1. **Aesthetic:** Reduce the visual length of the presented URL. + + 1. For a github repository, separating the `username/repo` into a separate group can be informative. + 2. Showing the initial and last characters of the URL, while hiding the middle using ellipses may make it more aesthetic at a glance, though it may hinder if a user wants to see the full URL. + +2. **Clarity:** Add an emoji indicating that `012345678901-ap-southeast-2-releases` is an S3 bucket. + +Compare the above with the following: + + +
+source + +Notes: + +1. The URL is shortened into the path after the `username/repo` This requires the `FileDownload` item to know that the first 2 segments is a group namespace. +2. The 🪣 bucket emoji clarified that the `012345678901-ap-southeast-2-releases` node represents an S3 bucket. diff --git a/doc/src/technical_concepts/diagrams/outcome/aesthetics_and_clarity/item_locations_improved.svg b/doc/src/technical_concepts/diagrams/outcome/aesthetics_and_clarity/item_locations_improved.svg new file mode 100644 index 000000000..2b3ab7278 --- /dev/null +++ b/doc/src/technical_concepts/diagrams/outcome/aesthetics_and_clarity/item_locations_improved.svg @@ -0,0 +1,140 @@ + + + + + + + + + +G + +cluster_tag_legend + +Legend + + +cluster_i02 + +web_app.zip: S3 Object + + +cluster_i01 + +web_app.zip: File Download + + +cluster_host___localhost + +💻 +localhost + + +cluster_host___github_com + +🌐 +github.com + + +cluster_host___github_com___azriel91__web_app + +🗄️ +azriel91/web_app + + +cluster_group___aws + +☁️ +AWS + + +cluster_group___aws___group___aws__ap_southeast_2 + +🌏 +ap-southeast-2 + + +cluster_group___aws___group___aws__ap_southeast_2___path___s3_bucket___012345678901_ap_southeast_2_releases + +🪣 +012345678901-ap-southeast-2-releases + + + + + + +host___localhost___path__to__web_app_zip + +📁 +/path/to/web_app.zip + + + +group___aws___group___aws__ap_southeast_2___path___s3_bucket___012345678901_ap_southeast_2_releases___web_app__0_1_1__web_app_zip + +📁 +/web_app/0.1.1/web_app.zip + + + + + +host___github_com___https__github_com__azriel91__web_app__releases__download__0_1_1__web_app_zip + +📁 +/releases/download/0.1.1/web_app.zip + + + + + + + diff --git a/doc/src/technical_concepts/diagrams/outcome/api_design.md b/doc/src/technical_concepts/diagrams/outcome/api_design.md new file mode 100644 index 000000000..752f1cf4b --- /dev/null +++ b/doc/src/technical_concepts/diagrams/outcome/api_design.md @@ -0,0 +1,177 @@ +# API Design + +> Designed around 2024-07-13 + + +## Requirements + +1. Be able to render a diagram, with/without the item existing. +2. Framework should be able to determine if an `ItemLocation` from B is: + 1. The same as an `ItemLocation` from A. + 2. Nested within an `ItemLocation` from A. + 3. Completely different. + +## Information Sources + +Known information before / during deployment: + +1. Flow edges -- `Edge::Contains` / `Edge::Logic`. Though maybe we want to reduce this to just `Edge::Logic`. +2. Flow item params that are specified. + +Missing information before / during deployment: + +1. State generated / produced / discovered through execution. +2. Parameters calculated from state. + + +## Desired Outcome + +> What it should look like. + +1. `Item` returns a `Vec`, where an `ItemInteraction` denotes a push/pull/within for a given source/dest. +1. We need `Item` implementors to render a diagram with `ParamsPartial`, or some generated. +2. However, partial params means `Item` implementors may not have the information to instantiate the `ItemLocation::{group, host, path}`s for the `Item`. + +### Single Item Example + +#### With values + +From this item: + +```yaml +items: + file_download: ! FileDownloadParams + src: "https://example.com/file.zip" + dest: "./target/abc/file.zip" +``` + +One of these diagrams is generated: + +
+ + +[source](https://azriel.im/dot_ix/#src=OYJwhgDgFg+gzgFwJ4BsCmAuABAIwPYAeWAxLoQDRYDGAliFegFAAmdaVCNeAdtgG5oQnKmBSMoNQWHpQkGRlixoCYALYR0MKnlXzFi5Wo1otOmADMamgF40I2AN4BfBVhR4RKKHkR797z29EGARpYDQEGDAcKgsrE1t7LGdGRm48ZhNuNTQ4P0N1TW1dLAAiAuMAOmLS1wqis0sbO2xSprRKxNrFANEghFberx8EbrcPPpGQsIiomLjmpNLQkHCEAHpoqnX2zrtatIyTTLgqPLqVQpNihYSWsqgEBAg89fX6juKd+L2IUpIsABXEBiHoTYbBFZrOaxdowRKtHaAlAodYQMAIKDrBB4bEzDZbb7oX4HdKZGBoVR4ABWNHOBkuxlMJX0rP0gB4NwAI+xcjA1VLd4fcsIBeDcAgzuuIb9bBsmUiwDdOxLwf1pqtZlsBQiReLGGhmOF6UpGXyNXYYDBJVMoWr5nDEmaQGgAI6A3IDLAAbQ+zJNEEoFsh+JhPoAujyrt7babzUrLYH1ZGIPbchAeHBMB6vTcE36YwHVZF4-FBRBQ4xQJAoHwaNYok8QAbdeEYKoaNx0NwDQzeddGkW7dHArH80GE0nna7sAAmVxd8NZvtR-2RK0Fm0LxMwB1wFMd9PTgx6450TuG7sR9dmpcq6GFxZjl2+XBgKgAa1SjeOuTOflIqjAL9yLAHXHRAsDAbhmCA5NUzQJRDzgLB0gQLA8AEEFIDDJl5zvAdJjzG81xw4CHzdAByRRSNSTFKUwVxEFQXI-EUMlP3MMBkQQE9FEQEA8AAmAAHcaGYTFsFIgBGSiZWUBBwDEqhgTgPAQAAWhTVsEEEIC8EBCDdRUlBgCk-QPxgTI2I4rjDVksBsAkZhMm4GdcBQF0om4Gg-005hsAAMhwVyTHAzyMV1ZzuKgSAe3cEBsACl1wqwHi+JMej0GwZgwDgKBdUUUhsuUzFwMg8xlI9XyAD50UxYMMHdZKAJUzLsukcA5AAZlDGUGpMISRKgMT3UnCACGDYzWR6+BIvJdIQD-FAxIANgABmW8b9CaFAprAGblPmsSAFZVvWhkbOwCqVMSxR3Uq6qoFq4KvLQFT6tklKmqy7xzHMNMEBUqkBBgcS4HNVs0GkGBW0sDzNK62UFPrZS1LwDTBGczNexwq8VxHC9NydEimP0AAeYnsAAKnioKPKe5hEpkuSsAuq7yqqjF7owR7QpenqPoK77fv+1DnodNC0xgdqQZQMGIah1saFhlmEaU1T1O4TSQHRo0e35UdcIhZc40Iu4Ny3Hc0yJxRSYpqn3JC7zEsmvrRKwUj3XEkaxvpggzqZy7ZVZu6Hpp7nXt4xrmq+n6IiFwHJdB7hwZASHuGhhW0DhmUbrZmq6t5yPWrAORJ0ztllaRtWNec6XgEePya8eRLNu23a5tEMTxOOpurC2lryW8NDDrW7uURbkxSoUvJXc74eZWbvugo4GgBA7rvure-iF5gWb9tdgAWNe2UmreB8EMT2sPiaN9S6bF84FfXaO2ej+v+BkHSrBmDwJ4wpldEHNbMAc+60Ma6EStbLA5MG4IEdpFCA0VlLYDUDgNGModIIGlonUwMVsBIDQCiPAAlq65gGOAsmkDoGwKitgxBLkEqMDOFPZmigAACAEkDmHAKoQC+dPp4AFjHAGsEHDOWWgAUn0A4JK18+ZR1+tgPeAAOAA3FgFwrIZ4SKkbw-m0c3TLVUS4Vw6isBsLQBwrhPCZGR34Xo2OIs0Bi2EaIiRihtHWL4QI-RhjnKaOSNI8Oz0bFeIUSotRxjGBAA) +
+ + +[source](https://azriel.im/dot_ix/#src=OYJwhgDgFg+gzgFwJ4BsCmAuABAIwPYAeWAxLoQDRYDGAliFegFAAmdaVCNeAdtgG5oQnKmBSMoNQWHpQkGRlixoCYALYR0MKnlXzFi5Wo1otOmADMamgF40I2AN4BfBVhR4RKKHkR797z29EGARpYDQEGDAcKgsrE1t7LGdGRm48ZhNuNTQ4P0N1TW1dLAAiAuMAOmLS1wqis0sbO2xSprRKxNrFANEghFberx8EbrcPPpGQsIiomLjmpNLQkHCEAHpoqnX2zrtatIyTTLgqPLqVQpNihYSWsqgEBAg89fX6juKd+L2IUpIsABXEBiHoTYbBFZrOaxdowRKtHaAlAodYQMAIKDrBB4bEzDZbb7oX4HdKZGBoVR4ABWNHOBkuxlMJX0rP0gB4NwAI+xcjA1VLd4fcsIBeDcAgzuuIb9bBsmUiwDdOxLwf1pqtZlsBQiReLGGhmOF6UpGXyNXYYDBJVMoWr5nDEmaQGgAI6A3IDLAAbQ+zJN9jgAHdKBbEABdHlXb2203mpWW-Ewn323IQHhwTAer03SO+tCBmOQuPqrMYbhoUOMUCQKB8GjWKJPEAG3XhUzcRDgGjcBAGhm866NeKCiBmoORK2RQsDu0wB3O13YcyiVN1PXHOjdw29iOTqMjlXQieLROz3y4MBUADWqUxlMwrkQqFyfkUZOOaAXyK7T-0bbw55MfpoZhMWwAByABGEDXFZZQEHAUCqGBOA8BAABaZMOwQQQsBAPBAW4TJmBQlBgEg6CVxgTJ3xQT8oP0GC4KwCRmEybhaJwFAXSibgaFUDFdWwAAydjOLAbjeMw5haMUOAoEgPt3BAbBhLQKSAR-P94GQdBsGYMAZN1RRSBk5DMVE5gsHMZCPQEgA+dFMWDDB3XUtAUN0mTpHAOQAGZQxlFyYAAoCoFA90ACYIAIYNSLZAKPPJdIQF4lBQIANgABnSmLWSaFB4FkhLkOS0CAFZMuyuiCFgsBsBslDVMUd1bPsqBHNEni+JQ5zYN-Vz3O8cxzFTBAUKpAQYDCuBzQ7NBpBgDtLG4zC-NlJq7IxVqnJcty9NkkAvIwMDyDC47ToAFlO1LyDA9KVplBCG2QtC8AwwRaIzftD2jQJY1VccbW3IdpydF1fFUgAecHsAAKmUriOok1SAqC4CsHAiqGWq2r6tlaz1ocjB2vE1zupwv8duMwbhtGvABBQh0BAbExvKmlAZrmhaOxoZaGrxlrHNJ3qKb2g6rpOi6Tsl67rtu3mHqQ1D0M7N7oKNPt+SzYc81HAsAa+h04GTVtbxlSGYbhom+Mk1k2eAR5BNtx5VNy-KwEKpLRFAm6sudqw8vikxvEZ0qfZlF2A4sDxATyNHvYxiy-dd8kz04AQvfKpGeo0iPEuKtGzoz-ys5MCOg8EUDvML2Li6TkwU5oNO0bK0Pq7JkutLTZg8CeXVVPRZiO2ACuKo+3QIahrBocdhAkdkiB5OQ7A1BwFW2VwhA2ZLUwFOwJA0BRPA-Vokcv0UM3J+n2e5O3xfcA4lTGDOGO6tcAABP8kHMcBVFyLBtv6vAVMIg0wEMkWi6UACk+gHB-2LsLQBQ0IjYDAmBVKABuLALhWTeygTA-+u0EHDWwOlDBWDMFvw-l-HIcBYFt3gUAkaY1XIM0EKmMBrJIHQNoULABDDiGkNojg5I3Dya8MQW6FB6DyGKBcEAA) +
+ +Which one is generated depends on how easy/difficult it is to figure out which node ports to use -- we don't necessarily know the node positions. + +Also, we may not necessarily use `dot`, but a `div_diag`. + + +#### Without values + +```yaml +items: + file_download: { src: "??", dest: "??" } +``` + + +### Multiple Items + +#### With values + +From these item params: + +```yaml +items: + app_file_download: !FileDownloadParams + src: "https://github.com/my_work_org/app/app_v1.zip" + dest: "./target/customer/app_v1/app_v1.zip" + + config_file_download: !FileDownloadParams + src: "https://my_work_org.internal/customer/app_v1/config.yaml" + dest: "./target/customer/app_v1/config.yaml" + + app_file_upload: !S3ObjectParams + file_path: "./target/customer/app_v1/app_v1.zip" + bucket_name: "my-work-org-12345-ap-southeast-2" + object_key: "/customer/solution/app/app_v1.zip" + + config_file_upload: !S3ObjectParams + file_path: "./target/customer/app_v1/config.yaml" + bucket_name: "my-work-org-12345-ap-southeast-2" + object_key: "/customer/solution/app/config.yaml" +``` + + + +[source](https://azriel.im/dot_ix/#src=LQhQBMEsCcFMGMAukD2A7AXAAgG62svAIYA2oAFpPkdPOQJ4ahZZEDuAzkyy+xwPocAzNgDEWAOYkUAI1JY0RALawOAByLxYAGiwyAroiwlIeDlkhpWWDpamwscCajTMeLcX0FD+RNYJRDclgiDkR+ACZud1ZOb34DeABrWHClen42FGgk-myJfgBGCKEAFgBWX38OQMRg0PCotxjYgWEE-WTU-nTM7Nz8opKKqoCgkLDI-nh9MJQVaACSQxcq-z9-HEL+AC9INWwAbwBfZpivdsSUtIysnLzoAuKyyr8xuonG6dnEefwllboNbTdAAM0gBXoyhIR1ObmcdX0MhBSmiHiwAGVyIESOAsGwHOQiHgLGhEPgVFAiOSFChwKosKDslgPlh8qw0Hi4GoUAB+M7iAAqwSwAAMAGKQEiwAAiKDYaGkRHAoos5KUNmx+lxaAA5EY4KQSPQbGoEJB5MQOLAAHRnBHkJEonq3foPAobYFbXb7WGgNy9O4DR78Szk6CKGFnQNuwZh-CR75zBZe7bwMEQ-hQpQwrAnf0saTEEjYsJo4woYul8KIGgSbozZP-T3elvbPYHPOndxF0jV-i1x4Nn5-RZtkFocGQ6F+0BoOmwfiKFRceGQRHI9OorAAIgdSJtW53a43zpj90GnvHHewO7bNo7x5Y5+DBXjEdIt5f7pt78jT6wb84zJBNSCTX4U3HdNJ0zbNcx3aCpxtOCAN7EsUDLXc0OrVDKz7DCazrYcmzHNRNm2a9fV3e9HzcbCCIHIjwkbCDmzI-hvUQ2CZ13LiJGQ6EAL4W8AEElCIHZ0CwAB1WAZExfAcEgLQOCEuJhFvDEhDUtofCubogJDZ4RjeGpxgaSIv3oYAg2AfJgGM8pgD8YAzI+BpgAiHT4n0m4+gvIzhleapanqSYInA0cAWQIEr3Y70b2o+LCgffZvMuTprhdfzXyGF5RjcsKvhYqKamWGK0GBPisx4hCM34lD-XEed6X4ekOHgVdxBYfdN3mbKg3dYFKM7HdyEQRA1C4AB6abesPeZpsMiRpo2VbktStRj26wDXQCt8QI-EhIsg5KJynGqc1vcbJpmpa9tfX9DsjaaSoWdbyNe+qBJzbaeHoyZB3rZiR1O8jUx9UabWmoHUle0H8A+jjCiRrZNr+ws8PQwGmJOtjwequDb2h2HEHhkjUZRvifpIDHWh8zKDIeobHIK0LPimN7-jKwFKri8HEp3cnWOgaaeYqj7KfR0AdouPTGb8wbBlZ0z2YsiKucWcXVk9QnauF0cxZQcqXEl6nGrnBd+FgJQUAAK0gVcevXR0+u3QAeDcABH2T1ds9mcvdiRuwQBeDcAQZ2A39kM-0-LAvYjnKhuj47NYhvWrqwYPABGduiserEPAG6dnOqwY0m8dI8Gg4z8PMeLnGhxBkjU-qy7cyztw5Y6LpFdjQL8tV8zwuwQA+DcAYr32-U+Wu4GnuniCtmB+KhGteN3nhrOxKw-H3TO6y5a8pMkKF85pfop19i09b7PQFgcB6ydita-Cfher9hOA-Wdf9n4fg4AAR30VQiByzAEkC7J0W5p77TXgLX0HA2BnBAQDIBaB4E13wnXYGZcIYjW-i-CBe9+bI0ht-OA6h0DWmAaA08+DI4ekDp-A41oEEP3QYReuWDxwjSLqw7+e8k4cLOhfEhsB-6AMoXw56YEU5QWbkTOBzCkEYBQdw7GbDMHSMEbI6EvDaGhkkcnE+MiYLThzMIshaAKHMIkeGRMGiCZaKukw9wiDc4l1xnYohF8VH9lLh41sDDv4d18pA3KKtD7uXClg7WsV6EwIOMwoJCsQksznv3CJi9G7RL5rEohHYFGuIwcREW2CGHeLcewvxaYHHHV8BPHeTM369wPu8Iqx9MkrwqlVaplDElTz3mElpHMNYnyyV04xLd8mP0YhUwxmjxlwX9BIaAfhyBKR2L4Ca0B77qBMGgVQ2AZjQDwOANwN96wTjCMssM980GqNwWAt2yT34Qw7MI0RmFQSkCcf9ApT9rGgQMY3IxF04JvIAR8r5sBTm30XFALZ5YkH3Oof1AhOSEpfx-iI8FQC9CaCSGcRFTyo76IEfY+Z2jMXvJxXIZIBLfnTPUbMiuATanb2Cf01J4TWnDPaSbGJH84nYBpfins9LfFMs8dUwJdT2W6IGYVIZUSOln38BfIVeL-QfBUNEMI9BpQ3KwOIM5sLYCfO1IgA1O0lK2BkFKdcjBSTWsgDIaUBKITjWwAAMhMBIcaZwWDghIMdDgRJWrzmgOJXMupCgAAYY26n9YyKUwbQ2LmxHgaA2BdTlHjYmwNKblSLiZI2LNsbc0tHzYIVNvgkCmFgKWuNCaWiXJQCkKthalzZEjVm0ojbE0trbSGjt6b8BZqEH25tiBoCtsXEO1qmhkB4CzTm8tMQB2zsQHq+tWBwAoAmjfRNGhwBQDQBIMdTaeB4PmOWFgAAeW92AABUPq-XNqJGaEE0hM1YDUPoaAahXXuH+UdG9WB71PpfYgft77Fzpi-dgJZRB6B0sfqB8DWBn3uqg2+vwsHjbZCFcsKF7hhKJvQ5h312G10wc-QR1gSgZD4DOHLNDD6MOQbzcm9tYau0x11BECd1HcO0e-coRj0BmMyqSRyvuXKhmsYg1h6Dwm4N0cQ8hoAA) + + +#### Without values + +TODO + + +## Learnings + +### State (Values) For Non-Existent Items + +Options: + +1. 🟡 **A:** Always get item implementors to return a value, but tagged as unknown or generated. + + Pros: + + 1. 🟢 Can always generate a diagram and show what might be, even if we don't actually have values to do so. + 2. 🟢 Whether using `dot` or `FlexDiag`, the diagram layout will more consistent from the clean state to the final state if nodes are styled to be invisible, rather than not present. + + Cons: + + 1. 🔴 What *is* generated can depend on input values, and if we have fake input values, we may generate an inaccurate diagram. + + 2. 🟡 Choosing a default example value of "application version X" may cause a subsequent item to fail, because the application version doesn't exist. + + Item implementors would be constrained to not make any external calls. + + 1. If we made every parameter value tagged with `!Example` vs `!Real`, then it can avoid this problem, but it is high maintenance on the item implementor. + 2. Maybe we pass in an additional parameter in `Item::apply_dry` so it isn't missed. + 3. Any `!Example` value used in a calculation can only produce `!Example` values. + 4. 🔵 If a dry run is intended to detect issues that *would* happen from an actual run, then we *do* want external systems to be called with the parameters passed in. e.g. detect credentials that are incorrect. + 5. 🔵 If a dry run is **NOT** intended to detect issues, then we will have simpler code implementation -- never make any external system calls (network, file IO). + + 3. 🟡 Choosing a default cloud provider in one item, could make a nonsensical diagram if another item uses a different cloud provider. + + Note that an `Item` may still generate sensible example state based on example parameter values. + + 4. 🔴 Code has complexity of either another derive macro generated type with `RealOrExample` for each state field, or a wrapper for `RealOrExample`. + +2. 🟡 **B:** Add `Item::state_example`, which provide fake state. + + Pros: + + 1. 🟢 Can always generate a diagram and show what might be, even if we don't actually have values to do so. + 2. 🟢 Whether using `dot` or `FlexDiag`, the diagram layout will more consistent from the clean state to the final state if nodes are styled to be invisible, rather than not present. + + Cons: + + 1. 🔴 Even more functions in the `Item` trait, creating more burden on item implementors. + 2. 🟡 Choosing a default cloud provider in one item, could make a nonsensical diagram if another item uses a different cloud provider. + + Note that an `Item` may still generate sensible example state based on example parameter values. + + +3. 🟡 **C:** Return `Option` for each value that is unknown. + + Pros: + + 1. 🟢 Never have false information in diagrams. + 2. 🟢 Code can put `None` for unknown values. + + Cons: + + 1. 🔴 Unable to generate useful diagram when starting from a clean state. i.e. cannot visualize the fully deployed state before deploying anything. + 2. 🔴 Code has complexity of another derive macro generated type with `Option` for each state field. + +Let's go with **B**. + + +### Notes From Designing Diagrams + +1. The node ID must be fully derivable from the `ItemLocation` / parameter / state, i.e. we cannot randomly insert a middle group's name in the middle of the ID. +2. The node ID must be namespaced as much as possible, so two same paths on different hosts / groups don't collide. +3. The node ID must NOT use the flow's graph edges (sequence / nesting) in its construction. This is because flows may evolve -- more items inserted before/after, and the node ID should be unaffected by those changes. diff --git a/doc/src/technical_concepts/diagrams/outcome/api_design/file_download_with_ports.svg b/doc/src/technical_concepts/diagrams/outcome/api_design/file_download_with_ports.svg new file mode 100644 index 000000000..5cc79f9a7 --- /dev/null +++ b/doc/src/technical_concepts/diagrams/outcome/api_design/file_download_with_ports.svg @@ -0,0 +1,68 @@ + + + + + + + +G + +cluster_localhost + +💻 +localhost + + +cluster_example_com + +🌐 +example.com + + + +localhost_target_abc_file_zip + +📁 +target/abc/file.zip +/full/path/to/target/abc/file.zip + + + +example_com_file_zip + +📁 +file.zip +https://example.com/file.zip + + + +example_com_file_zip:sw->localhost_target_abc_file_zip + + + + + +example_com_file_zip:se->localhost_target_abc_file_zip:ne + + + + + diff --git a/doc/src/technical_concepts/diagrams/outcome/api_design/file_download_with_spaces.svg b/doc/src/technical_concepts/diagrams/outcome/api_design/file_download_with_spaces.svg new file mode 100644 index 000000000..3dade77a0 --- /dev/null +++ b/doc/src/technical_concepts/diagrams/outcome/api_design/file_download_with_spaces.svg @@ -0,0 +1,68 @@ + + + + + + + +G + +cluster_localhost + +💻 +localhost + + +cluster_example_com + +🌐 +example.com + + + +localhost_target_abc_file_zip + +📁 +target/abc/file.zip +/full/path/to/target/abc/file.zip + + + +example_com_file_zip + +📁 +file.zip +https://example.com/file.zip + + + +example_com_file_zip->localhost_target_abc_file_zip + + +   + + + +example_com_file_zip->localhost_target_abc_file_zip + + + + + diff --git a/doc/src/technical_concepts/diagrams/outcome/api_design/multiple_items.svg b/doc/src/technical_concepts/diagrams/outcome/api_design/multiple_items.svg new file mode 100644 index 000000000..0a8d8da45 --- /dev/null +++ b/doc/src/technical_concepts/diagrams/outcome/api_design/multiple_items.svg @@ -0,0 +1,150 @@ + + + + + + + + + +G + +cluster_aws + +Amazon Web Services + + +cluster_aws_s3 + +S3 + + +cluster_aws_s3_bucket_my_work_org_12345_ap_southeast_2 + +🪣 +my-work-org-12345-ap-southeast-2 + + +cluster_github_com + +🌐 +github.com + + +cluster_my_work_org_internal + +🌐 +my_work_org.internal + + +cluster_localhost + +💻 +localhost + + + +aws_s3_bucket_my_work_org_12345_ap_southeast_2_customer_solution_app_app_v1_zip + +📁 +app_v1.zip + + + +localhost_target_customer_app_v1_app_v1_zip + +📁 +app_v1.zip + + + +aws_s3_bucket_my_work_org_12345_ap_southeast_2_customer_solution_app_app_v1_zip->localhost_target_customer_app_v1_app_v1_zip + + + + + +aws_s3_bucket_my_work_org_12345_ap_southeast_2_customer_solution_app_config_yaml + +📄 +config.yaml + + + +localhost_target_customer_app_v1_config_yaml + +📄 +config.yaml + + + +aws_s3_bucket_my_work_org_12345_ap_southeast_2_customer_solution_app_config_yaml->localhost_target_customer_app_v1_config_yaml + + + + + +github_com_my_work_org_app_app_v1_zip + +📁 +app_v1.zip + + + +github_com_my_work_org_app_app_v1_zip:sw->localhost_target_customer_app_v1_app_v1_zip:nw + + + + + +github_com_my_work_org_app_app_v1_zip:se->localhost_target_customer_app_v1_app_v1_zip + + + + + +my_work_org_internal_customer_app_v1_config_yaml + +📄 +config.yaml + + + +my_work_org_internal_customer_app_v1_config_yaml:sw->localhost_target_customer_app_v1_app_v1_zip:nw + + + + + +my_work_org_internal_customer_app_v1_config_yaml:se->localhost_target_customer_app_v1_config_yaml + + + + + diff --git a/doc/src/technical_concepts/diagrams/outcome/interaction_merging.md b/doc/src/technical_concepts/diagrams/outcome/interaction_merging.md new file mode 100644 index 000000000..8b2c91353 --- /dev/null +++ b/doc/src/technical_concepts/diagrams/outcome/interaction_merging.md @@ -0,0 +1,53 @@ +# Interaction Merging + +#### 1. Begin with multiple items' `ItemLocations` + + +
+source + + +#### 2. Merge matching `ItemLocation`s + + +
+source + + +#### 3. Hide Edges (`ItemInteraction`s) + + +
+source + + +#### 4. Assign Nodes (`ItemLocation`s) and (`ItemInteraction`s) Edges to Tags + + +
+source + + +#### 5. Animate and Show Edges (`ItemInteraction`s) For Each Step + + +
+source + +This can be used for both the example deployed state, as well as realtime execution. + diff --git a/doc/src/technical_concepts/diagrams/outcome/interaction_merging/item_locations_edges_hidden.svg b/doc/src/technical_concepts/diagrams/outcome/interaction_merging/item_locations_edges_hidden.svg new file mode 100644 index 000000000..2bf245883 --- /dev/null +++ b/doc/src/technical_concepts/diagrams/outcome/interaction_merging/item_locations_edges_hidden.svg @@ -0,0 +1,99 @@ + + + + + + + + + +G + +cluster_host___localhost + +localhost + + +cluster_host___github_com + +github.com + + +cluster_group___aws + +AWS + + +cluster_group___aws___group___aws__ap_southeast_2 + +ap-southeast-2 + + +cluster_group___aws___group___aws__ap_southeast_2___path___s3_bucket___012345678901_ap_southeast_2_releases + +012345678901-ap-southeast-2-releases + + + +host___localhost___path__to__web_app_zip + +/path/to/web_app.zip + + + +group___aws___group___aws__ap_southeast_2___path___s3_bucket___012345678901_ap_southeast_2_releases___web_app__0_1_1__web_app_zip + +/web_app/0.1.1/web_app.zip + + + + + +host___github_com___https__github_com__azriel91__web_app__releases__download__0_1_1__web_app_zip + +https://github.com/azriel91/web_app/releases/download/0.1.1/web_app.zip + + + + + + + diff --git a/doc/src/technical_concepts/diagrams/outcome/interaction_merging/item_locations_individual.svg b/doc/src/technical_concepts/diagrams/outcome/interaction_merging/item_locations_individual.svg new file mode 100644 index 000000000..398ec5ded --- /dev/null +++ b/doc/src/technical_concepts/diagrams/outcome/interaction_merging/item_locations_individual.svg @@ -0,0 +1,110 @@ + + + + + + + + + +G + +cluster_i01_client_localhost + +localhost + + +cluster_i01_server_host + +github.com + + +cluster_i02__from_localhost + +localhost + + +cluster_i02__aws + +AWS + + +cluster_i02__aws__ap_southeast_2 + +ap-southeast-2 + + +cluster_i02__aws__ap_southeast_2__to_bucket + +012345678901-ap-southeast-2-releases + + + +i01_client_localhost_path + +/path/to/web_app.zip + + + +i01_server_host_url + +https://github.com/azriel91/web_app/releases/download/0.1.1/web_app.zip + + + +i01_server_host_url->i01_client_localhost_path + + + + + +i01_server_host_url->i01_client_localhost_path + + + + + +i02__from_localhost_path + +/path/to/web_app.zip + + + +i02__aws__ap_southeast_2__to_bucket_path + +/web_app/0.1.1/web_app.zip + + + +i02__from_localhost_path->i02__aws__ap_southeast_2__to_bucket_path + + + + + diff --git a/doc/src/technical_concepts/diagrams/outcome/interaction_merging/item_locations_merged.svg b/doc/src/technical_concepts/diagrams/outcome/interaction_merging/item_locations_merged.svg new file mode 100644 index 000000000..1f1822bc2 --- /dev/null +++ b/doc/src/technical_concepts/diagrams/outcome/interaction_merging/item_locations_merged.svg @@ -0,0 +1,99 @@ + + + + + + + + + +G + +cluster_host___localhost + +localhost + + +cluster_host___github_com + +github.com + + +cluster_group___aws + +AWS + + +cluster_group___aws___group___aws__ap_southeast_2 + +ap-southeast-2 + + +cluster_group___aws___group___aws__ap_southeast_2___path___s3_bucket___012345678901_ap_southeast_2_releases + +012345678901-ap-southeast-2-releases + + + +host___localhost___path__to__web_app_zip + +/path/to/web_app.zip + + + +group___aws___group___aws__ap_southeast_2___path___s3_bucket___012345678901_ap_southeast_2_releases___web_app__0_1_1__web_app_zip + +/web_app/0.1.1/web_app.zip + + + +host___localhost___path__to__web_app_zip->group___aws___group___aws__ap_southeast_2___path___s3_bucket___012345678901_ap_southeast_2_releases___web_app__0_1_1__web_app_zip + + + + + +host___github_com___https__github_com__azriel91__web_app__releases__download__0_1_1__web_app_zip + +https://github.com/azriel91/web_app/releases/download/0.1.1/web_app.zip + + + +host___github_com___https__github_com__azriel91__web_app__releases__download__0_1_1__web_app_zip->host___localhost___path__to__web_app_zip + + + + + +host___github_com___https__github_com__azriel91__web_app__releases__download__0_1_1__web_app_zip->host___localhost___path__to__web_app_zip + + + + + diff --git a/doc/src/technical_concepts/diagrams/outcome/interaction_merging/item_locations_tagged.svg b/doc/src/technical_concepts/diagrams/outcome/interaction_merging/item_locations_tagged.svg new file mode 100644 index 000000000..3712d6ed0 --- /dev/null +++ b/doc/src/technical_concepts/diagrams/outcome/interaction_merging/item_locations_tagged.svg @@ -0,0 +1,117 @@ + + + + + + + + + +G + +cluster_tag_legend + +Legend + + +cluster_i02 + +web_app.zip: S3 Object + + +cluster_i01 + +web_app.zip: File Download + + +cluster_host___localhost + +localhost + + +cluster_host___github_com + +github.com + + +cluster_group___aws + +AWS + + +cluster_group___aws___group___aws__ap_southeast_2 + +ap-southeast-2 + + +cluster_group___aws___group___aws__ap_southeast_2___path___s3_bucket___012345678901_ap_southeast_2_releases + +012345678901-ap-southeast-2-releases + + + + + + +host___localhost___path__to__web_app_zip + +/path/to/web_app.zip + + + +group___aws___group___aws__ap_southeast_2___path___s3_bucket___012345678901_ap_southeast_2_releases___web_app__0_1_1__web_app_zip + +/web_app/0.1.1/web_app.zip + + + + + +host___github_com___https__github_com__azriel91__web_app__releases__download__0_1_1__web_app_zip + +https://github.com/azriel91/web_app/releases/download/0.1.1/web_app.zip + + + + + + + diff --git a/doc/src/technical_concepts/diagrams/outcome/interaction_merging/item_locations_tagged_animated.svg b/doc/src/technical_concepts/diagrams/outcome/interaction_merging/item_locations_tagged_animated.svg new file mode 100644 index 000000000..b1a276093 --- /dev/null +++ b/doc/src/technical_concepts/diagrams/outcome/interaction_merging/item_locations_tagged_animated.svg @@ -0,0 +1,126 @@ + + + + + + + + + +G + +cluster_tag_legend + +Legend + + +cluster_i02 + +web_app.zip: S3 Object + + +cluster_i01 + +web_app.zip: File Download + + +cluster_host___localhost + +localhost + + +cluster_host___github_com + +github.com + + +cluster_group___aws + +AWS + + +cluster_group___aws___group___aws__ap_southeast_2 + +ap-southeast-2 + + +cluster_group___aws___group___aws__ap_southeast_2___path___s3_bucket___012345678901_ap_southeast_2_releases + +012345678901-ap-southeast-2-releases + + + + + + +host___localhost___path__to__web_app_zip + +/path/to/web_app.zip + + + +group___aws___group___aws__ap_southeast_2___path___s3_bucket___012345678901_ap_southeast_2_releases___web_app__0_1_1__web_app_zip + +/web_app/0.1.1/web_app.zip + + + + + +host___github_com___https__github_com__azriel91__web_app__releases__download__0_1_1__web_app_zip + +https://github.com/azriel91/web_app/releases/download/0.1.1/web_app.zip + + + + + + + diff --git a/examples/download/Cargo.toml b/examples/download/Cargo.toml index c05ebd874..afa450af3 100644 --- a/examples/download/Cargo.toml +++ b/examples/download/Cargo.toml @@ -19,25 +19,27 @@ crate-type = ["cdylib", "rlib"] [dependencies] peace_items = { path = "../../items", features = ["file_download"] } -thiserror = "1.0.57" -url = { version = "2.5.0", features = ["serde"] } +thiserror = "1.0.63" +url = { version = "2.5.2", features = ["serde"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] peace = { workspace = true, default-features = false, features = ["cli"] } -clap = { version = "4.5.2", features = ["derive"] } -tokio = { version = "1.36.0", features = ["net", "time", "rt"] } +clap = { version = "4.5.16", features = ["derive"] } +tokio = { version = "1.40.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.5" -tokio = "1.36.0" -wasm-bindgen = "0.2.92" -wasm-bindgen-futures = "0.4.42" -js-sys = "0.3.69" -web-sys = "0.3.69" +tokio = "1.40.0" +wasm-bindgen = "0.2.95" +wasm-bindgen-futures = "0.4.43" +js-sys = "0.3.70" +web-sys = "0.3.70" [features] default = [] error_reporting = ["peace/error_reporting", "peace_items/error_reporting"] output_progress = ["peace/output_progress", "peace_items/output_progress"] +item_interactions = ["peace/item_interactions", "peace_items/item_interactions"] +item_state_example = ["peace/item_state_example", "peace_items/item_state_example"] diff --git a/examples/download/src/lib.rs b/examples/download/src/lib.rs index 7b952f929..14fce966a 100644 --- a/examples/download/src/lib.rs +++ b/examples/download/src/lib.rs @@ -86,7 +86,7 @@ where let mut cmd_ctx_builder = CmdCtx::builder_single_profile_single_flow(output.into(), workspace.into()) .with_profile(profile) - .with_flow(flow); + .with_flow(flow.into()); if let Some(file_download_params) = file_download_params { cmd_ctx_builder = cmd_ctx_builder.with_item_params::>( diff --git a/examples/envman/Cargo.toml b/examples/envman/Cargo.toml index 852302b4b..1931204da 100644 --- a/examples/envman/Cargo.toml +++ b/examples/envman/Cargo.toml @@ -18,54 +18,54 @@ test = false crate-type = ["cdylib", "rlib"] [dependencies] -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.22.0", optional = true } +aws-config = { version = "1.5.5", optional = true } +aws-sdk-iam = { version = "1.42.0", optional = true } +aws-sdk-s3 = { version = "1.47.0", optional = true } +aws-smithy-types = { version = "1.2.4", optional = true } # used to reference error type, otherwise not recommended for direct usage +base64 = { version = "0.22.1", optional = true } cfg-if = "1.0.0" -chrono = { version = "0.4.35", default-features = false, features = ["clock", "serde"], optional = true } +chrono = { version = "0.4.38", 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"] } -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"] } +semver = { version = "1.0.23", optional = true } +serde = { version = "1.0.209", features = ["derive"] } +thiserror = { version = "1.0.63", optional = true } +url = { version = "2.5.2", features = ["serde"] } urlencoding = { version = "2.1.3", optional = true } -whoami = { version = "1.5.0", optional = true } +whoami = { version = "1.5.1", optional = true } # web_server # ssr -axum = { version = "0.7.4", optional = true } -hyper = { version = "1.2.0", optional = true } -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 } +axum = { version = "0.7.5", optional = true } +hyper = { version = "1.4.1", optional = true } +leptos = { version = "0.6.14", default-features = false, features = ["serde"] } +leptos_axum = { version = "0.6.14", optional = true } +leptos_meta = { version = "0.6.14", default-features = false } +leptos_router = { version = "0.6.14", default-features = false } +tower = { version = "0.5.0", optional = true } 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.2", features = ["derive"], optional = true } -tokio = { version = "1.36.0", features = ["rt", "rt-multi-thread", "signal"], optional = true } +clap = { version = "4.5.16", features = ["derive"], optional = true } +tokio = { version = "1.40.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.21" +log = "0.4.22" serde-wasm-bindgen = "0.6.5" -tokio = "1.36.0" -wasm-bindgen = "0.2.92" -wasm-bindgen-futures = "0.4.42" -js-sys = "0.3.69" -web-sys = "0.3.69" +tokio = "1.40.0" +wasm-bindgen = "0.2.95" +wasm-bindgen-futures = "0.4.43" +js-sys = "0.3.70" +web-sys = "0.3.70" [features] -default = [] +default = ["item_interactions", "item_state_example"] # === envman modes === # cli = [ @@ -115,6 +115,14 @@ output_progress = [ "peace/output_progress", "peace_items/output_progress", ] +item_interactions = [ + "peace/item_interactions", + "peace_items/item_interactions", +] +item_state_example = [ + "peace/item_state_example", + "peace_items/item_state_example", +] # === envman low level === # flow_logic = [ diff --git a/examples/envman/src/cmds/app_upload_cmd.rs b/examples/envman/src/cmds/app_upload_cmd.rs index 5ebcb9ab5..48138b7e0 100644 --- a/examples/envman/src/cmds/app_upload_cmd.rs +++ b/examples/envman/src/cmds/app_upload_cmd.rs @@ -60,13 +60,13 @@ impl AppUploadCmd { let mut cmd_ctx = { let cmd_ctx_builder = CmdCtx::builder_single_profile_single_flow::( output.into(), - (&workspace).into(), + workspace.into(), ); crate::cmds::ws_and_profile_params_augment!(cmd_ctx_builder); cmd_ctx_builder - .with_profile_from_workspace_param(&profile_key) - .with_flow(&flow) + .with_profile_from_workspace_param(profile_key.into()) + .with_flow((&flow).into()) .with_item_params::>( item_id!("s3_object"), s3_object_params_spec, @@ -125,7 +125,7 @@ impl AppUploadCmd { crate::cmds::ws_and_profile_params_augment!(cmd_ctx_builder); cmd_ctx_builder - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::>( item_id!("s3_object"), s3_object_params_spec, diff --git a/examples/envman/src/cmds/env_cmd.rs b/examples/envman/src/cmds/env_cmd.rs index d4635ddf7..73179771c 100644 --- a/examples/envman/src/cmds/env_cmd.rs +++ b/examples/envman/src/cmds/env_cmd.rs @@ -29,19 +29,10 @@ use crate::{ pub struct EnvCmd; impl EnvCmd { - /// Runs a command on the environment with the active profile. - /// - /// # Parameters - /// - /// * `output`: Output to write the execution outcome. - /// * `cmd_opts`: Options to configure the `Cmd`'s output. - /// * `f`: The command to run. - pub async fn run(output: &mut O, cmd_opts: CmdOpts, f: F) -> Result + /// Returns the `CmdCtx` for the `EnvDeployFlow`. + pub async fn cmd_ctx(output: &mut O) -> Result, EnvManError> where O: OutputWrite, - for<'fn_once> F: FnOnce( - &'fn_once mut EnvManCmdCtx<'_, O>, - ) -> LocalBoxFuture<'fn_once, Result>, { let workspace = Workspace::new( app_name!(), @@ -52,30 +43,46 @@ impl EnvCmd { )?; let flow = EnvDeployFlow::flow().await?; let profile_key = WorkspaceParamsKey::Profile; - let iam_role_path = String::from("/"); let iam_role_params_spec = IamRoleParams::::field_wise_spec() .with_name_from_map(|profile: &Profile| Some(profile.to_string())) .with_path(iam_role_path) .with_managed_policy_arn_from_map(IamPolicyState::policy_id_arn_version) .build(); - - let CmdOpts { profile_print } = cmd_opts; - - let mut cmd_ctx = { + let cmd_ctx = { let cmd_ctx_builder = CmdCtx::builder_single_profile_single_flow::( output.into(), - (&workspace).into(), + workspace.into(), ); crate::cmds::interruptibility_augment!(cmd_ctx_builder); crate::cmds::ws_and_profile_params_augment!(cmd_ctx_builder); cmd_ctx_builder - .with_profile_from_workspace_param(&profile_key) - .with_flow(&flow) + .with_profile_from_workspace_param(profile_key.into()) + .with_flow(flow.into()) .with_item_params::>(item_id!("iam_role"), iam_role_params_spec) .await? }; + Ok(cmd_ctx) + } + + /// Runs a command on the environment with the active profile. + /// + /// # Parameters + /// + /// * `output`: Output to write the execution outcome. + /// * `cmd_opts`: Options to configure the `Cmd`'s output. + /// * `f`: The command to run. + pub async fn run(output: &mut O, cmd_opts: CmdOpts, f: F) -> Result + where + O: OutputWrite, + for<'fn_once> F: FnOnce( + &'fn_once mut EnvManCmdCtx<'_, O>, + ) -> LocalBoxFuture<'fn_once, Result>, + { + let mut cmd_ctx = Self::cmd_ctx(output).await?; + + let CmdOpts { profile_print } = cmd_opts; if profile_print { Self::profile_print(&mut cmd_ctx).await?; @@ -116,7 +123,7 @@ impl EnvCmd { crate::cmds::interruptibility_augment!(cmd_ctx_builder); crate::cmds::ws_and_profile_params_augment!(cmd_ctx_builder); - cmd_ctx_builder.with_flow(&flow).await? + cmd_ctx_builder.with_flow((&flow).into()).await? }; let t = f(&mut cmd_ctx).await?; diff --git a/examples/envman/src/cmds/env_diff_cmd.rs b/examples/envman/src/cmds/env_diff_cmd.rs index 7a1fb3e8e..f520ae507 100644 --- a/examples/envman/src/cmds/env_diff_cmd.rs +++ b/examples/envman/src/cmds/env_diff_cmd.rs @@ -113,7 +113,7 @@ impl EnvDiffCmd { output .present(&( - Heading::new(HeadingLevel::Level1, "States Cleaned"), + Heading::new(HeadingLevel::Level1, "State Diffs"), states_diffs_presentables, "\n", )) diff --git a/examples/envman/src/cmds/profile_init_cmd.rs b/examples/envman/src/cmds/profile_init_cmd.rs index 7e8a43905..299fdf543 100644 --- a/examples/envman/src/cmds/profile_init_cmd.rs +++ b/examples/envman/src/cmds/profile_init_cmd.rs @@ -219,8 +219,8 @@ where crate::cmds::ws_and_profile_params_augment!(cmd_ctx_builder); cmd_ctx_builder - .with_profile_from_workspace_param(profile_key) - .with_flow(flow) + .with_profile_from_workspace_param(profile_key.into()) + .with_flow(flow.into()) .with_item_params::>( item_id!("app_download"), app_download_params_spec, @@ -262,8 +262,8 @@ where crate::cmds::ws_and_profile_params_augment!(cmd_ctx_builder); cmd_ctx_builder - .with_profile_from_workspace_param(profile_key) - .with_flow(flow) + .with_profile_from_workspace_param(profile_key.into()) + .with_flow(flow.into()) .with_item_params::>( item_id!("app_download"), app_download_params_spec, diff --git a/examples/envman/src/cmds/profile_show_cmd.rs b/examples/envman/src/cmds/profile_show_cmd.rs index 21c8adcc1..7f42486ea 100644 --- a/examples/envman/src/cmds/profile_show_cmd.rs +++ b/examples/envman/src/cmds/profile_show_cmd.rs @@ -33,13 +33,13 @@ impl ProfileShowCmd { let cmd_ctx_builder = CmdCtx::builder_single_profile_no_flow::( output.into(), - (&workspace).into(), + workspace.into(), ); crate::cmds::ws_and_profile_params_augment!(cmd_ctx_builder); let profile_key = WorkspaceParamsKey::Profile; let mut cmd_ctx = cmd_ctx_builder - .with_profile_from_workspace_param(&profile_key) + .with_profile_from_workspace_param(profile_key.into()) .await?; let SingleProfileNoFlowView { output, diff --git a/examples/envman/src/flows/env_deploy_flow.rs b/examples/envman/src/flows/env_deploy_flow.rs index 5ab07d58c..e068292a1 100644 --- a/examples/envman/src/flows/env_deploy_flow.rs +++ b/examples/envman/src/flows/env_deploy_flow.rs @@ -31,21 +31,15 @@ impl EnvDeployFlow { let graph = { let mut graph_builder = ItemGraphBuilder::::new(); - let [ - app_download_id, - iam_policy_item_id, - iam_role_item_id, - instance_profile_item_id, - s3_bucket_id, - s3_object_id, - ] = graph_builder.add_fns([ - FileDownloadItem::::new(item_id!("app_download")).into(), - IamPolicyItem::::new(item_id!("iam_policy")).into(), - IamRoleItem::::new(item_id!("iam_role")).into(), - InstanceProfileItem::::new(item_id!("instance_profile")).into(), - S3BucketItem::::new(item_id!("s3_bucket")).into(), - S3ObjectItem::::new(item_id!("s3_object")).into(), - ]); + let [app_download_id, iam_policy_item_id, iam_role_item_id, instance_profile_item_id, s3_bucket_id, s3_object_id] = + graph_builder.add_fns([ + FileDownloadItem::::new(item_id!("app_download")).into(), + IamPolicyItem::::new(item_id!("iam_policy")).into(), + IamRoleItem::::new(item_id!("iam_role")).into(), + InstanceProfileItem::::new(item_id!("instance_profile")).into(), + S3BucketItem::::new(item_id!("s3_bucket")).into(), + S3ObjectItem::::new(item_id!("s3_object")).into(), + ]); graph_builder.add_logic_edges([ (iam_policy_item_id, iam_role_item_id), diff --git a/examples/envman/src/items/peace_aws_iam_policy/iam_policy_item.rs b/examples/envman/src/items/peace_aws_iam_policy/iam_policy_item.rs index ed1c79255..5e36db95c 100644 --- a/examples/envman/src/items/peace_aws_iam_policy/iam_policy_item.rs +++ b/examples/envman/src/items/peace_aws_iam_policy/iam_policy_item.rs @@ -78,6 +78,32 @@ where Ok(()) } + #[cfg(feature = "item_state_example")] + fn state_example(params: &Self::Params<'_>, _data: Self::Data<'_>) -> Self::State { + use peace::cfg::state::Generated; + + use crate::items::peace_aws_iam_policy::model::PolicyIdArnVersion; + + let name = params.name().to_string(); + let path = params.path().to_string(); + let policy_document = params.policy_document().to_string(); + let policy_id_arn_version = { + let aws_account_id = "123456789012"; // Can this be looked up without calling AWS? + let id = String::from("iam_role_example_id"); + let arn = format!("arn:aws:iam::{aws_account_id}:policy/{name}"); + let version = String::from("v1"); + + Generated::Value(PolicyIdArnVersion::new(id, arn, version)) + }; + + IamPolicyState::Some { + name, + path, + policy_document, + policy_id_arn_version, + } + } + async fn try_state_current( fn_ctx: FnCtx<'_>, params_partial: & as Params>::Partial, @@ -159,4 +185,26 @@ where IamPolicyApplyFns::::apply(fn_ctx, params, data, state_current, state_target, diff) .await } + + #[cfg(feature = "item_interactions")] + fn interactions( + params: &Self::Params<'_>, + _data: Self::Data<'_>, + ) -> Vec { + use peace::item_model::{ItemInteractionPush, ItemLocation, ItemLocationAncestors}; + + let iam_policy_name = format!("📝 {}", params.name()); + + let item_interaction = ItemInteractionPush::new( + ItemLocationAncestors::new(vec![ItemLocation::localhost()]), + ItemLocationAncestors::new(vec![ + ItemLocation::group(String::from("IAM")), + ItemLocation::group(String::from("Policies")), + ItemLocation::path(iam_policy_name), + ]), + ) + .into(); + + vec![item_interaction] + } } diff --git a/examples/envman/src/items/peace_aws_iam_policy/iam_policy_state.rs b/examples/envman/src/items/peace_aws_iam_policy/iam_policy_state.rs index 99a60d5aa..096304286 100644 --- a/examples/envman/src/items/peace_aws_iam_policy/iam_policy_state.rs +++ b/examples/envman/src/items/peace_aws_iam_policy/iam_policy_state.rs @@ -5,6 +5,9 @@ use serde::{Deserialize, Serialize}; use crate::items::peace_aws_iam_policy::model::PolicyIdArnVersion; +#[cfg(feature = "output_progress")] +use peace::item_model::ItemLocationState; + /// Instance profile state. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] pub enum IamPolicyState { @@ -76,3 +79,13 @@ impl fmt::Display for IamPolicyState { } } } + +#[cfg(feature = "output_progress")] +impl<'state> From<&'state IamPolicyState> for ItemLocationState { + fn from(iam_policy_state: &'state IamPolicyState) -> ItemLocationState { + match iam_policy_state { + IamPolicyState::Some { .. } => ItemLocationState::Exists, + IamPolicyState::None => ItemLocationState::NotExists, + } + } +} diff --git a/examples/envman/src/items/peace_aws_iam_policy/iam_policy_state_goal_fn.rs b/examples/envman/src/items/peace_aws_iam_policy/iam_policy_state_goal_fn.rs index 3df85a959..216130ca5 100644 --- a/examples/envman/src/items/peace_aws_iam_policy/iam_policy_state_goal_fn.rs +++ b/examples/envman/src/items/peace_aws_iam_policy/iam_policy_state_goal_fn.rs @@ -56,6 +56,8 @@ where path: String, policy_document: String, ) -> Result { + // TODO: `Generated::Tbd` is should be saved as `Generated::Value` at the point + // of applying a change. let policy_id_arn_version = Generated::Tbd; Ok(IamPolicyState::Some { diff --git a/examples/envman/src/items/peace_aws_iam_role/iam_role_item.rs b/examples/envman/src/items/peace_aws_iam_role/iam_role_item.rs index 0c36183e6..3993efd28 100644 --- a/examples/envman/src/items/peace_aws_iam_role/iam_role_item.rs +++ b/examples/envman/src/items/peace_aws_iam_role/iam_role_item.rs @@ -78,6 +78,33 @@ where Ok(()) } + #[cfg(feature = "item_state_example")] + fn state_example(params: &Self::Params<'_>, _data: Self::Data<'_>) -> Self::State { + use peace::cfg::state::Generated; + + use crate::items::peace_aws_iam_role::model::{ManagedPolicyAttachment, RoleIdAndArn}; + + let name = params.name().to_string(); + let path = params.path().to_string(); + let aws_account_id = "123456789012"; // Can this be looked up without calling AWS? + let role_id_and_arn = { + let id = String::from("iam_role_example_id"); + let arn = format!("arn:aws:iam::{aws_account_id}:role/{name}"); + Generated::Value(RoleIdAndArn::new(id, arn)) + }; + let managed_policy_attachment = { + let arn = params.managed_policy_arn().to_string(); + ManagedPolicyAttachment::new(Generated::Value(arn), true) + }; + + IamRoleState::Some { + name, + path, + role_id_and_arn, + managed_policy_attachment, + } + } + async fn try_state_current( fn_ctx: FnCtx<'_>, params_partial: & as Params>::Partial, @@ -158,4 +185,26 @@ where ) -> Result { IamRoleApplyFns::::apply(fn_ctx, params, data, state_current, state_target, diff).await } + + #[cfg(feature = "item_interactions")] + fn interactions( + params: &Self::Params<'_>, + _data: Self::Data<'_>, + ) -> Vec { + use peace::item_model::{ItemInteractionPush, ItemLocation, ItemLocationAncestors}; + + let iam_role_name = format!("🧢 {}", params.name()); + + let item_interaction = ItemInteractionPush::new( + ItemLocationAncestors::new(vec![ItemLocation::localhost()]), + ItemLocationAncestors::new(vec![ + ItemLocation::group(String::from("IAM")), + ItemLocation::group(String::from("Roles")), + ItemLocation::path(iam_role_name), + ]), + ) + .into(); + + vec![item_interaction] + } } diff --git a/examples/envman/src/items/peace_aws_iam_role/iam_role_state.rs b/examples/envman/src/items/peace_aws_iam_role/iam_role_state.rs index 8e05a1884..fd13d8d36 100644 --- a/examples/envman/src/items/peace_aws_iam_role/iam_role_state.rs +++ b/examples/envman/src/items/peace_aws_iam_role/iam_role_state.rs @@ -5,6 +5,9 @@ use serde::{Deserialize, Serialize}; use crate::items::peace_aws_iam_role::model::{ManagedPolicyAttachment, RoleIdAndArn}; +#[cfg(feature = "output_progress")] +use peace::item_model::ItemLocationState; + /// IAM role state. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] pub enum IamRoleState { @@ -67,3 +70,13 @@ impl fmt::Display for IamRoleState { } } } + +#[cfg(feature = "output_progress")] +impl<'state> From<&'state IamRoleState> for ItemLocationState { + fn from(iam_role_state: &'state IamRoleState) -> ItemLocationState { + match iam_role_state { + IamRoleState::Some { .. } => ItemLocationState::Exists, + IamRoleState::None => ItemLocationState::NotExists, + } + } +} diff --git a/examples/envman/src/items/peace_aws_iam_role/iam_role_state_diff.rs b/examples/envman/src/items/peace_aws_iam_role/iam_role_state_diff.rs index 2dfd3006b..1c6029d45 100644 --- a/examples/envman/src/items/peace_aws_iam_role/iam_role_state_diff.rs +++ b/examples/envman/src/items/peace_aws_iam_role/iam_role_state_diff.rs @@ -1,5 +1,6 @@ use std::fmt; +use peace::cfg::state::Generated; use serde::{Deserialize, Serialize}; use crate::items::peace_aws_iam_role::model::ManagedPolicyAttachment; @@ -47,8 +48,19 @@ impl fmt::Display for IamRoleStateDiff { managed_policy_attachment_current, managed_policy_attachment_goal, } => { - if managed_policy_attachment_current.arn() != managed_policy_attachment_goal.arn() { - write!(f, "Managed policy attachment will be replaced.") + let current_arn = managed_policy_attachment_current.arn(); + let goal_arn = managed_policy_attachment_goal.arn(); + if current_arn != goal_arn { + match (current_arn, goal_arn) { + (Generated::Value(_), Generated::Value(_)) | + (Generated::Tbd, Generated::Value(_)) | + (Generated::Tbd, Generated::Tbd) => { + write!(f, "Managed policy attachment will be replaced.") + } + // TODO: not always true. + (Generated::Value(_), Generated::Tbd) + => write!(f, "exists and is up to date."), + } } else { match ( managed_policy_attachment_current.attached(), diff --git a/examples/envman/src/items/peace_aws_instance_profile/instance_profile_item.rs b/examples/envman/src/items/peace_aws_instance_profile/instance_profile_item.rs index 32d9a5282..6de8df23b 100644 --- a/examples/envman/src/items/peace_aws_instance_profile/instance_profile_item.rs +++ b/examples/envman/src/items/peace_aws_instance_profile/instance_profile_item.rs @@ -79,6 +79,26 @@ where Ok(()) } + #[cfg(feature = "item_state_example")] + fn state_example(params: &Self::Params<'_>, _data: Self::Data<'_>) -> Self::State { + use peace::cfg::state::Generated; + + use crate::items::peace_aws_instance_profile::model::InstanceProfileIdAndArn; + + let name = params.name().to_string(); + let path = params.path().to_string(); + let aws_account_id = "123456789012"; // Can this be looked up without calling AWS? + let id = String::from("instance_profile_example_id"); + let arn = format!("arn:aws:iam::{aws_account_id}:instance-profile/{name}"); + + InstanceProfileState::Some { + name, + path, + instance_profile_id_and_arn: Generated::Value(InstanceProfileIdAndArn::new(id, arn)), + role_associated: true, + } + } + async fn try_state_current( fn_ctx: FnCtx<'_>, params_partial: & as Params>::Partial, @@ -175,4 +195,26 @@ where ) .await } + + #[cfg(feature = "item_interactions")] + fn interactions( + params: &Self::Params<'_>, + _data: Self::Data<'_>, + ) -> Vec { + use peace::item_model::{ItemInteractionPush, ItemLocation, ItemLocationAncestors}; + + let instance_profile_name = format!("📝 {}", params.name()); + + let item_interaction = ItemInteractionPush::new( + ItemLocationAncestors::new(vec![ItemLocation::localhost()]), + ItemLocationAncestors::new(vec![ + ItemLocation::group(String::from("IAM")), + ItemLocation::group(String::from("Instance Profiles")), + ItemLocation::path(instance_profile_name), + ]), + ) + .into(); + + vec![item_interaction] + } } diff --git a/examples/envman/src/items/peace_aws_instance_profile/instance_profile_state.rs b/examples/envman/src/items/peace_aws_instance_profile/instance_profile_state.rs index 6aa0c6665..8443d62d9 100644 --- a/examples/envman/src/items/peace_aws_instance_profile/instance_profile_state.rs +++ b/examples/envman/src/items/peace_aws_instance_profile/instance_profile_state.rs @@ -5,6 +5,9 @@ use serde::{Deserialize, Serialize}; use crate::items::peace_aws_instance_profile::model::InstanceProfileIdAndArn; +#[cfg(feature = "output_progress")] +use peace::item_model::ItemLocationState; + /// Instance profile state. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] pub enum InstanceProfileState { @@ -67,3 +70,13 @@ impl fmt::Display for InstanceProfileState { } } } + +#[cfg(feature = "output_progress")] +impl<'state> From<&'state InstanceProfileState> for ItemLocationState { + fn from(instance_profile_state: &'state InstanceProfileState) -> ItemLocationState { + match instance_profile_state { + InstanceProfileState::Some { .. } => ItemLocationState::Exists, + InstanceProfileState::None => ItemLocationState::NotExists, + } + } +} diff --git a/examples/envman/src/items/peace_aws_instance_profile/instance_profile_state_current_fn.rs b/examples/envman/src/items/peace_aws_instance_profile/instance_profile_state_current_fn.rs index 2b844e7aa..ba83759c4 100644 --- a/examples/envman/src/items/peace_aws_instance_profile/instance_profile_state_current_fn.rs +++ b/examples/envman/src/items/peace_aws_instance_profile/instance_profile_state_current_fn.rs @@ -84,7 +84,7 @@ where let instance_profile_id_and_arn = InstanceProfileIdAndArn::new(instance_profile_id, instance_profile_arn); - let role_associated = instance_profile.roles().first().is_some(); + let role_associated = !instance_profile.roles().is_empty(); Some(( instance_profile_name, diff --git a/examples/envman/src/items/peace_aws_s3_bucket/s3_bucket_item.rs b/examples/envman/src/items/peace_aws_s3_bucket/s3_bucket_item.rs index 07de489f9..f4e124707 100644 --- a/examples/envman/src/items/peace_aws_s3_bucket/s3_bucket_item.rs +++ b/examples/envman/src/items/peace_aws_s3_bucket/s3_bucket_item.rs @@ -79,6 +79,17 @@ where Ok(()) } + #[cfg(feature = "item_state_example")] + fn state_example(params: &Self::Params<'_>, _data: Self::Data<'_>) -> Self::State { + use chrono::Utc; + use peace::cfg::state::Timestamped; + + S3BucketState::Some { + name: params.name().to_string(), + creation_date: Timestamped::Value(Utc::now()), + } + } + async fn try_state_current( fn_ctx: FnCtx<'_>, params_partial: & as Params>::Partial, @@ -159,4 +170,25 @@ where ) -> Result { S3BucketApplyFns::::apply(fn_ctx, params, data, state_current, state_target, diff).await } + + #[cfg(feature = "item_interactions")] + fn interactions( + params: &Self::Params<'_>, + _data: Self::Data<'_>, + ) -> Vec { + use peace::item_model::{ItemInteractionPush, ItemLocation, ItemLocationAncestors}; + + let s3_bucket_name = format!("🪣 {}", params.name()); + + let item_interaction = ItemInteractionPush::new( + ItemLocationAncestors::new(vec![ItemLocation::localhost()]), + ItemLocationAncestors::new(vec![ + ItemLocation::group(String::from("S3")), + ItemLocation::path(s3_bucket_name), + ]), + ) + .into(); + + vec![item_interaction] + } } 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 d9a690472..27d8c7766 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 @@ -4,6 +4,9 @@ use chrono::{DateTime, Utc}; use peace::cfg::state::Timestamped; use serde::{Deserialize, Serialize}; +#[cfg(feature = "output_progress")] +use peace::item_model::ItemLocationState; + /// S3 bucket state. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] pub enum S3BucketState { @@ -53,3 +56,13 @@ impl fmt::Display for S3BucketState { } } } + +#[cfg(feature = "output_progress")] +impl<'state> From<&'state S3BucketState> for ItemLocationState { + fn from(s3_bucket_state: &'state S3BucketState) -> ItemLocationState { + match s3_bucket_state { + S3BucketState::Some { .. } => ItemLocationState::Exists, + S3BucketState::None => ItemLocationState::NotExists, + } + } +} diff --git a/examples/envman/src/items/peace_aws_s3_object/s3_object_item.rs b/examples/envman/src/items/peace_aws_s3_object/s3_object_item.rs index 324b665c7..143c01fc4 100644 --- a/examples/envman/src/items/peace_aws_s3_object/s3_object_item.rs +++ b/examples/envman/src/items/peace_aws_s3_object/s3_object_item.rs @@ -79,6 +79,40 @@ where Ok(()) } + #[cfg(feature = "item_state_example")] + fn state_example(params: &Self::Params<'_>, _data: Self::Data<'_>) -> Self::State { + use std::fmt::Write; + + use peace::cfg::state::Generated; + + let example_content = b"s3_object_example"; + + let content_md5_hexstr = { + let content_md5_bytes = { + let mut md5_ctx = md5_rs::Context::new(); + md5_ctx.read(example_content); + md5_ctx.finish() + }; + content_md5_bytes + .iter() + .try_fold( + String::with_capacity(content_md5_bytes.len() * 2), + |mut hexstr, x| { + write!(&mut hexstr, "{:02x}", x)?; + Result::<_, std::fmt::Error>::Ok(hexstr) + }, + ) + .expect("Failed to construct hexstring from S3 object MD5.") + }; + + S3ObjectState::Some { + bucket_name: params.bucket_name().to_string(), + object_key: params.object_key().to_string(), + content_md5_hexstr: Some(content_md5_hexstr.clone()), + e_tag: Generated::Value(content_md5_hexstr), + } + } + async fn try_state_current( fn_ctx: FnCtx<'_>, params_partial: & as Params>::Partial, @@ -159,4 +193,30 @@ where ) -> Result { S3ObjectApplyFns::::apply(fn_ctx, params, data, state_current, state_target, diff).await } + + #[cfg(feature = "item_interactions")] + fn interactions( + params: &Self::Params<'_>, + _data: Self::Data<'_>, + ) -> Vec { + use peace::item_model::{ItemInteractionPush, ItemLocation, ItemLocationAncestors}; + + let file_path = format!("📄 {}", params.file_path().display()); + let bucket_name = format!("🪣 {}", params.bucket_name()); + let object_name = format!("📄 {}", params.object_key()); + + let item_interaction = ItemInteractionPush::new( + ItemLocationAncestors::new(vec![ + ItemLocation::localhost(), + ItemLocation::path(file_path), + ]), + ItemLocationAncestors::new(vec![ + ItemLocation::path(bucket_name), + ItemLocation::path(object_name), + ]), + ) + .into(); + + vec![item_interaction] + } } diff --git a/examples/envman/src/items/peace_aws_s3_object/s3_object_state.rs b/examples/envman/src/items/peace_aws_s3_object/s3_object_state.rs index a046de685..ef06ef35d 100644 --- a/examples/envman/src/items/peace_aws_s3_object/s3_object_state.rs +++ b/examples/envman/src/items/peace_aws_s3_object/s3_object_state.rs @@ -3,6 +3,9 @@ use std::fmt; use peace::cfg::state::Generated; use serde::{Deserialize, Serialize}; +#[cfg(feature = "output_progress")] +use peace::item_model::ItemLocationState; + /// S3 object state. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] pub enum S3ObjectState { @@ -52,3 +55,13 @@ impl fmt::Display for S3ObjectState { } } } + +#[cfg(feature = "output_progress")] +impl<'state> From<&'state S3ObjectState> for ItemLocationState { + fn from(s3_object_state: &'state S3ObjectState) -> ItemLocationState { + match s3_object_state { + S3ObjectState::Some { .. } => ItemLocationState::Exists, + S3ObjectState::None => ItemLocationState::NotExists, + } + } +} diff --git a/examples/envman/src/items/peace_aws_s3_object/s3_object_state_current_fn.rs b/examples/envman/src/items/peace_aws_s3_object/s3_object_state_current_fn.rs index 6012af55d..e434c872b 100644 --- a/examples/envman/src/items/peace_aws_s3_object/s3_object_state_current_fn.rs +++ b/examples/envman/src/items/peace_aws_s3_object/s3_object_state_current_fn.rs @@ -34,7 +34,7 @@ where .await .map(Some) } else { - Ok(None) + Ok(Some(S3ObjectState::None)) } } diff --git a/examples/envman/src/lib.rs b/examples/envman/src/lib.rs index 598c0f016..580a9db27 100644 --- a/examples/envman/src/lib.rs +++ b/examples/envman/src/lib.rs @@ -63,20 +63,26 @@ cfg_if::cfg_if! { use wasm_bindgen::prelude::wasm_bindgen; use leptos::*; - use peace::webi_components::Home; + use peace::webi_components::{ChildrenFn, Home}; #[wasm_bindgen] pub async fn hydrate() { + use crate::web_components::EnvDeployHome; // initializes logging using the `log` crate let _log = console_log::init_with_level(log::Level::Debug); console_error_panic_hook::set_once(); + let app_home = ChildrenFn::new(EnvDeployHome); + leptos::mount_to_body(move || { view! { - + } }); } } } + +#[cfg(any(feature = "web_server", feature = "hydrate"))] +pub mod web_components; diff --git a/examples/envman/src/main_cli.rs b/examples/envman/src/main_cli.rs index a79901a12..9c8add8d1 100644 --- a/examples/envman/src/main_cli.rs +++ b/examples/envman/src/main_cli.rs @@ -3,7 +3,7 @@ use std::net::SocketAddr; use clap::Parser; use envman::{ cmds::{ - EnvCleanCmd, EnvDeployCmd, EnvDiffCmd, EnvDiscoverCmd, EnvGoalCmd, EnvStatusCmd, + CmdOpts, EnvCleanCmd, EnvDeployCmd, EnvDiffCmd, EnvDiscoverCmd, EnvGoalCmd, EnvStatusCmd, ProfileInitCmd, ProfileListCmd, ProfileShowCmd, ProfileSwitchCmd, }, model::{ @@ -40,6 +40,12 @@ pub fn run() -> Result<(), EnvManError> { let mut builder = CliOutput::builder().with_colorize(color); if let Some(format) = format { builder = builder.with_outcome_format(format); + + #[cfg(feature = "output_progress")] + { + use peace::cli::output::CliProgressFormatOpt; + builder = builder.with_progress_format(CliProgressFormatOpt::Outcome); + } } builder.build() @@ -128,14 +134,150 @@ 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 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?; + use futures::FutureExt; + use peace::{ + cmd::scopes::SingleProfileSingleFlowView, + cmd_model::CmdOutcome, + webi::output::{CmdExecSpawnCtx, FlowWebiFns, WebiServer}, + webi_components::ChildrenFn, + }; + + use envman::{ + cmds::EnvCmd, + flows::EnvDeployFlow, + web_components::{CmdExecRequest, EnvDeployHome}, + }; + + let flow = EnvDeployFlow::flow() + .await + .expect("Failed to instantiate EnvDeployFlow."); + + let flow_webi_fns = FlowWebiFns { + flow: flow.clone(), + outcome_info_graph_fn: Box::new(|webi_output, outcome_info_graph_gen| { + async move { + let mut cmd_ctx = EnvCmd::cmd_ctx(webi_output) + .await + .expect("Expected CmdCtx to be successfully constructed."); + + // TODO: consolidate the `flow` above with this? + let SingleProfileSingleFlowView { + flow, + params_specs, + resources, + .. + } = cmd_ctx.view(); + + outcome_info_graph_gen(flow, params_specs, resources) + } + .boxed_local() + }), + cmd_exec_spawn_fn: Box::new(|mut webi_output, cmd_exec_request| { + use peace::rt::cmds::{ + ApplyStoredStateSync, CleanCmd, EnsureCmd, StatesDiscoverCmd, + }; + let cmd_exec_task = async move { + let mut cli_output = CliOutput::builder().build(); + let cli_output = &mut cli_output; + + let (cmd_error, item_errors) = match cmd_exec_request { + CmdExecRequest::Discover => { + eprintln!("Running discover."); + let result = + EnvCmd::run(&mut webi_output, CmdOpts::default(), |cmd_ctx| { + async { StatesDiscoverCmd::current_and_goal(cmd_ctx).await } + .boxed_local() + }) + .await; + + match result { + Ok(cmd_outcome) => { + if let CmdOutcome::ItemError { errors, .. } = cmd_outcome { + (None, Some(errors)) + } else { + (None, None) + } + } + Err(error) => (Some(error), None), + } + } + CmdExecRequest::Ensure => { + eprintln!("Running ensure."); + let result = + EnvCmd::run(&mut webi_output, CmdOpts::default(), |cmd_ctx| { + async { + EnsureCmd::exec_with( + cmd_ctx, + ApplyStoredStateSync::Current, + ) + .await + } + .boxed_local() + }) + .await; + + match result { + Ok(cmd_outcome) => { + if let CmdOutcome::ItemError { errors, .. } = cmd_outcome { + (None, Some(errors)) + } else { + (None, None) + } + } + Err(error) => (Some(error), None), + } + } + CmdExecRequest::Clean => { + eprintln!("Running clean."); + let result = + EnvCmd::run(&mut webi_output, CmdOpts::default(), |cmd_ctx| { + async { + CleanCmd::exec_with( + cmd_ctx, + ApplyStoredStateSync::Current, + ) + .await + } + .boxed_local() + }) + .await; + + match result { + Ok(cmd_outcome) => { + if let CmdOutcome::ItemError { errors, .. } = cmd_outcome { + (None, Some(errors)) + } else { + (None, None) + } + } + Err(error) => (Some(error), None), + } + } + }; + + if let Some(cmd_error) = cmd_error { + let _ = envman::output::errors_present(cli_output, &[cmd_error]).await; + } + if let Some(item_errors) = item_errors.as_ref() { + let _ = + envman::output::item_errors_present(cli_output, item_errors).await; + } + } + .boxed_local(); + + CmdExecSpawnCtx { + interrupt_tx: None, + cmd_exec_task, + } + }), + }; + + WebiServer::start( + Some(SocketAddr::from((address, port))), + ChildrenFn::new(EnvDeployHome), + flow_webi_fns, + ) + .await?; } } diff --git a/examples/envman/src/web_components.rs b/examples/envman/src/web_components.rs new file mode 100644 index 000000000..92e9a3760 --- /dev/null +++ b/examples/envman/src/web_components.rs @@ -0,0 +1,9 @@ +#![allow(non_snake_case)] // Components are all PascalCase. + +pub use self::{ + cmd_exec_request::CmdExecRequest, env_deploy_home::EnvDeployHome, tab_label::TabLabel, +}; + +mod cmd_exec_request; +mod env_deploy_home; +mod tab_label; diff --git a/examples/envman/src/web_components/cmd_exec_request.rs b/examples/envman/src/web_components/cmd_exec_request.rs new file mode 100644 index 000000000..640ac4e3a --- /dev/null +++ b/examples/envman/src/web_components/cmd_exec_request.rs @@ -0,0 +1,12 @@ +use serde::{Deserialize, Serialize}; + +/// Request for a command execution. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum CmdExecRequest { + /// Run the `StatesDiscoverCmd`. + Discover, + /// Run the `EnsureCmd`. + Ensure, + /// Run the `CleanCmd`. + Clean, +} diff --git a/examples/envman/src/web_components/env_deploy_home.rs b/examples/envman/src/web_components/env_deploy_home.rs new file mode 100644 index 000000000..c2a3b0fec --- /dev/null +++ b/examples/envman/src/web_components/env_deploy_home.rs @@ -0,0 +1,174 @@ +use leptos::{component, server, spawn_local, view, IntoView, ServerFnError}; +use peace::webi_components::{FlowGraph, FlowGraphCurrent}; + +use crate::web_components::TabLabel; + +#[server] +async fn discover_cmd_exec() -> Result<(), ServerFnError> { + use tokio::sync::mpsc; + + use crate::web_components::CmdExecRequest; + + let cmd_exec_request_tx = leptos::use_context::>(); + + if let Some(cmd_exec_request_tx) = cmd_exec_request_tx { + match cmd_exec_request_tx.try_send(CmdExecRequest::Discover) { + Ok(()) => {} + Err(e) => { + leptos::logging::log!("Failed to send Discover cmd: {e}"); + } + } + } else { + leptos::logging::log!("`cmd_exec_request_tx` is None"); + } + + Ok(()) +} + +#[server] +async fn deploy_cmd_exec() -> Result<(), ServerFnError> { + use tokio::sync::mpsc; + + use crate::web_components::CmdExecRequest; + + let cmd_exec_request_tx = leptos::use_context::>(); + + if let Some(cmd_exec_request_tx) = cmd_exec_request_tx { + match cmd_exec_request_tx.try_send(CmdExecRequest::Ensure) { + Ok(()) => {} + Err(e) => { + leptos::logging::log!("Failed to send Ensure cmd: {e}"); + } + } + } else { + leptos::logging::log!("`cmd_exec_request_tx` is None"); + } + + Ok(()) +} + +#[server] +async fn clean_cmd_exec() -> Result<(), ServerFnError> { + use tokio::sync::mpsc; + + use crate::web_components::CmdExecRequest; + + let cmd_exec_request_tx = leptos::use_context::>(); + + if let Some(cmd_exec_request_tx) = cmd_exec_request_tx { + match cmd_exec_request_tx.try_send(CmdExecRequest::Clean) { + Ok(()) => {} + Err(e) => { + leptos::logging::log!("Failed to send Clean cmd: {e}"); + } + } + } else { + leptos::logging::log!("`cmd_exec_request_tx` is None"); + } + + Ok(()) +} + +/// Top level component of the `WebiOutput`. +#[component] +pub fn EnvDeployHome() -> impl IntoView { + let button_tw_classes = "\ + border \ + rounded \ + px-4 \ + py-3 \ + text-m \ + \ + border-slate-400 \ + bg-gradient-to-b \ + from-slate-200 \ + to-slate-300 \ + \ + hover:border-slate-300 \ + hover:bg-gradient-to-b \ + hover:from-slate-100 \ + hover:to-slate-200 \ + \ + active:border-slate-500 \ + active:bg-gradient-to-b \ + active:from-slate-300 \ + active:to-slate-400 \ + "; + + view! { +
+

"Environment"

+ + + + + + + + +
+ } +} diff --git a/examples/envman/src/web_components/tab_label.rs b/examples/envman/src/web_components/tab_label.rs new file mode 100644 index 000000000..bdba8cf2d --- /dev/null +++ b/examples/envman/src/web_components/tab_label.rs @@ -0,0 +1,59 @@ +use leptos::{component, ev::Event, html, view, Callable, Callback, IntoView, NodeRef}; + +/// The label that users click to switch to that tab. +#[component] +pub fn TabLabel( + tab_group_name: &'static str, + tab_id: &'static str, + #[prop(default = "")] label: &'static str, + #[prop(default = "")] class: &'static str, + #[prop(default = false)] checked: bool, + #[prop(into, optional, default = None)] on_change: Option>, + #[prop(into, optional, default = leptos::create_node_ref::())] node_ref: NodeRef< + html::Input, + >, +) -> impl IntoView { + let tab_classes = format!( + "\ + peer/{tab_id} \ + hidden \ + " + ); + + let label_classes = format!( + "\ + {class} \ + \ + inline-block \ + h-7 \ + lg:h-9 \ + px-2.5 \ + \ + cursor-pointer \ + border-t \ + border-x \ + border-slate-400 \ + \ + peer-checked/{tab_id}:border-t-4 \ + peer-checked/{tab_id}:border-t-blue-500 \ + " + ); + + #[cfg(not(target_arch = "wasm32"))] + let _node_ref = node_ref; + + view! { + + + } +} diff --git a/items/Cargo.toml b/items/Cargo.toml index 6c1caf9e0..9b913accd 100644 --- a/items/Cargo.toml +++ b/items/Cargo.toml @@ -49,6 +49,20 @@ output_progress = [ "peace_item_sh_cmd?/output_progress", "peace_item_tar_x?/output_progress", ] +item_interactions = [ + "peace/item_interactions", + "peace_item_blank?/item_interactions", + "peace_item_file_download?/item_interactions", + "peace_item_sh_cmd?/item_interactions", + "peace_item_tar_x?/item_interactions", +] +item_state_example = [ + "peace/item_state_example", + "peace_item_blank?/item_state_example", + "peace_item_file_download?/item_state_example", + "peace_item_sh_cmd?/item_state_example", + "peace_item_tar_x?/item_state_example", +] # Subcrates blank = ["dep:peace_item_blank"] diff --git a/items/blank/Cargo.toml b/items/blank/Cargo.toml index f01748a3f..c81b1b470 100644 --- a/items/blank/Cargo.toml +++ b/items/blank/Cargo.toml @@ -30,3 +30,5 @@ thiserror = { workspace = true } default = [] error_reporting = ["peace/error_reporting"] output_progress = ["peace/output_progress"] +item_interactions = ["peace/item_interactions"] +item_state_example = ["peace/item_state_example"] diff --git a/items/blank/src/blank_item.rs b/items/blank/src/blank_item.rs index 7fd4d2d1d..c7cc4f11a 100644 --- a/items/blank/src/blank_item.rs +++ b/items/blank/src/blank_item.rs @@ -63,6 +63,11 @@ where Ok(()) } + #[cfg(feature = "item_state_example")] + fn state_example(params: &Self::Params<'_>, _data: Self::Data<'_>) -> Self::State { + BlankState(params.dest.0) + } + async fn try_state_current( _fn_ctx: FnCtx<'_>, params_partial: & as Params>::Partial, @@ -157,4 +162,17 @@ where ) -> Result { BlankApplyFns::::apply(fn_ctx, params, data, state_current, state_target, diff).await } + + #[cfg(feature = "item_interactions")] + fn interactions( + _params: &Self::Params<'_>, + _data: Self::Data<'_>, + ) -> Vec { + use peace::item_model::{ItemInteractionWithin, ItemLocation}; + + let item_interaction = + ItemInteractionWithin::new(vec![ItemLocation::localhost()].into()).into(); + + vec![item_interaction] + } } diff --git a/items/blank/src/blank_state.rs b/items/blank/src/blank_state.rs index e828788b1..e6e62dc1a 100644 --- a/items/blank/src/blank_state.rs +++ b/items/blank/src/blank_state.rs @@ -2,6 +2,9 @@ use std::fmt; use serde::{Deserialize, Serialize}; +#[cfg(feature = "output_progress")] +use peace::item_model::ItemLocationState; + /// Logical blank state. #[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)] pub struct BlankState(pub Option); @@ -28,3 +31,13 @@ impl std::ops::DerefMut for BlankState { &mut self.0 } } + +#[cfg(feature = "output_progress")] +impl<'state> From<&'state BlankState> for ItemLocationState { + fn from(blank_state: &'state BlankState) -> ItemLocationState { + match blank_state.is_some() { + true => ItemLocationState::Exists, + false => ItemLocationState::NotExists, + } + } +} diff --git a/items/file_download/Cargo.toml b/items/file_download/Cargo.toml index 25f02951c..84e2129be 100644 --- a/items/file_download/Cargo.toml +++ b/items/file_download/Cargo.toml @@ -40,3 +40,5 @@ tokio = { workspace = true } default = [] error_reporting = ["peace/error_reporting"] output_progress = ["peace/output_progress"] +item_interactions = ["peace/item_interactions"] +item_state_example = ["peace/item_state_example"] diff --git a/items/file_download/src/file_download_apply_fns.rs b/items/file_download/src/file_download_apply_fns.rs index e61fc5743..4dfeaa66e 100644 --- a/items/file_download/src/file_download_apply_fns.rs +++ b/items/file_download/src/file_download_apply_fns.rs @@ -18,7 +18,7 @@ use reqwest::header::ETAG; use crate::{ ETag, FileDownloadData, FileDownloadError, FileDownloadParams, FileDownloadState, - FileDownloadStateDiff, + FileDownloadStateDiff, FileDownloadStateLogical, }; #[cfg(feature = "output_progress")] @@ -240,11 +240,11 @@ where pub async fn apply_check( _params: &FileDownloadParams, _data: FileDownloadData<'_, Id>, - State { + FileDownloadState(State { logical: file_state_current, physical: _e_tag, - }: &State>, - _file_download_state_goal: &State>, + }): &FileDownloadState, + _file_download_state_goal: &FileDownloadState, diff: &FileDownloadStateDiff, ) -> Result { let apply_check = match diff { @@ -273,8 +273,8 @@ where } } FileDownloadStateDiff::Deleted { .. } => match file_state_current { - FileDownloadState::None { .. } => ApplyCheck::ExecNotRequired, - FileDownloadState::StringContents { + FileDownloadStateLogical::None { .. } => ApplyCheck::ExecNotRequired, + FileDownloadStateLogical::StringContents { path: _, #[cfg(not(feature = "output_progress"))] contents: _, @@ -294,7 +294,7 @@ where } } } - FileDownloadState::Length { + FileDownloadStateLogical::Length { path: _, #[cfg(not(feature = "output_progress"))] byte_count: _, @@ -311,7 +311,7 @@ where progress_limit: ProgressLimit::Bytes(*byte_count), } } - FileDownloadState::Unknown { path: _ } => { + FileDownloadStateLogical::Unknown { path: _ } => { #[cfg(not(feature = "output_progress"))] { ApplyCheck::ExecRequired @@ -333,10 +333,10 @@ where _fn_ctx: FnCtx<'_>, _params: &FileDownloadParams, _data: FileDownloadData<'_, Id>, - _file_download_state_current: &State>, - file_download_state_goal: &State>, + _file_download_state_current: &FileDownloadState, + file_download_state_goal: &FileDownloadState, _diff: &FileDownloadStateDiff, - ) -> Result>, FileDownloadError> { + ) -> Result { // TODO: fetch headers but don't write to file. Ok(file_download_state_goal.clone()) @@ -346,10 +346,10 @@ where fn_ctx: FnCtx<'_>, params: &FileDownloadParams, data: FileDownloadData<'_, Id>, - _file_download_state_current: &State>, - file_download_state_goal: &State>, + _file_download_state_current: &FileDownloadState, + file_download_state_goal: &FileDownloadState, diff: &FileDownloadStateDiff, - ) -> Result>, FileDownloadError> { + ) -> Result { match diff { FileDownloadStateDiff::Deleted { path } => { #[cfg(feature = "output_progress")] @@ -371,7 +371,7 @@ where let e_tag = Self::file_download(fn_ctx, params, data).await?; let mut file_download_state_ensured = file_download_state_goal.clone(); - file_download_state_ensured.physical = e_tag; + file_download_state_ensured.0.physical = e_tag; Ok(file_download_state_ensured) } diff --git a/items/file_download/src/file_download_data.rs b/items/file_download/src/file_download_data.rs index d74676a35..8212450ac 100644 --- a/items/file_download/src/file_download_data.rs +++ b/items/file_download/src/file_download_data.rs @@ -4,11 +4,11 @@ use std::marker::PhantomData; use peace::rt_model::Storage; use peace::{ - cfg::{accessors::Stored, state::FetchedOpt, State}, + cfg::accessors::Stored, data::{accessors::R, marker::Current, Data}, }; -use crate::{ETag, FileDownloadState}; +use crate::FileDownloadState; /// Data used to download a file. /// @@ -25,10 +25,10 @@ where client: R<'exec, reqwest::Client>, /// The previous file download state. - state_prev: Stored<'exec, State>>, + state_prev: Stored<'exec, FileDownloadState>, /// The file state working copy in memory. - state_working: R<'exec, Current>>>, + state_working: R<'exec, Current>, /// For wasm, we write to web storage through the `Storage` object. /// @@ -49,11 +49,11 @@ where &self.client } - pub fn state_prev(&self) -> &Stored<'exec, State>> { + pub fn state_prev(&self) -> &Stored<'exec, FileDownloadState> { &self.state_prev } - pub fn state_working(&self) -> &Current>> { + pub fn state_working(&self) -> &Current { &self.state_working } diff --git a/items/file_download/src/file_download_item.rs b/items/file_download/src/file_download_item.rs index 87e9d2f11..efd99c4d0 100644 --- a/items/file_download/src/file_download_item.rs +++ b/items/file_download/src/file_download_item.rs @@ -1,15 +1,15 @@ use std::{marker::PhantomData, path::Path}; use peace::{ - cfg::{async_trait, state::FetchedOpt, ApplyCheck, FnCtx, Item, ItemId, State}, + cfg::{async_trait, state::FetchedOpt, ApplyCheck, FnCtx, Item, ItemId}, params::Params, resource_rt::{resources::ts::Empty, Resources}, }; use crate::{ - ETag, FileDownloadApplyFns, FileDownloadData, FileDownloadError, FileDownloadParams, + FileDownloadApplyFns, FileDownloadData, FileDownloadError, FileDownloadParams, FileDownloadState, FileDownloadStateCurrentFn, FileDownloadStateDiff, FileDownloadStateDiffFn, - FileDownloadStateGoalFn, + FileDownloadStateGoalFn, FileDownloadStateLogical, }; /// Item for downloading a file. @@ -56,7 +56,7 @@ where type Data<'exec> = FileDownloadData<'exec, Id>; type Error = FileDownloadError; type Params<'exec> = FileDownloadParams; - type State = State>; + type State = FileDownloadState; type StateDiff = FileDownloadStateDiff; fn id(&self) -> &ItemId { @@ -69,6 +69,19 @@ where Ok(()) } + #[cfg(feature = "item_state_example")] + fn state_example(params: &Self::Params<'_>, _data: Self::Data<'_>) -> Self::State { + let dest = params.dest(); + + FileDownloadState::new( + FileDownloadStateLogical::StringContents { + path: dest.to_path_buf(), + contents: "example contents".to_string(), + }, + FetchedOpt::None, + ) + } + async fn try_state_current( fn_ctx: FnCtx<'_>, params_partial: & as Params>::Partial, @@ -115,7 +128,8 @@ where _data: Self::Data<'_>, ) -> Result { let path = params_partial.dest().map(Path::to_path_buf); - let state = State::new(FileDownloadState::None { path }, FetchedOpt::Tbd); + let state = + FileDownloadState::new(FileDownloadStateLogical::None { path }, FetchedOpt::Tbd); Ok(state) } @@ -160,4 +174,32 @@ where FileDownloadApplyFns::::apply(fn_ctx, params, data, state_current, state_target, diff) .await } + + #[cfg(feature = "item_interactions")] + fn interactions( + params: &Self::Params<'_>, + _data: Self::Data<'_>, + ) -> Vec { + use peace::item_model::{ItemInteractionPull, ItemLocation, ItemLocationAncestors}; + + let location_server: ItemLocationAncestors = vec![ + ItemLocation::host_from_url(params.src()), + ItemLocation::path(params.src().to_string()), + ] + .into(); + + let location_client: ItemLocationAncestors = vec![ + ItemLocation::localhost(), + ItemLocation::path(format!("📄 {}", params.dest().display())), + ] + .into(); + + let item_interaction = ItemInteractionPull { + location_client, + location_server, + } + .into(); + + vec![item_interaction] + } } diff --git a/items/file_download/src/file_download_state.rs b/items/file_download/src/file_download_state.rs index 19878ef3c..4fa921b25 100644 --- a/items/file_download/src/file_download_state.rs +++ b/items/file_download/src/file_download_state.rs @@ -1,149 +1,47 @@ -use std::{fmt, path::PathBuf}; +use std::fmt; +use peace::cfg::{state::FetchedOpt, State}; use serde::{Deserialize, Serialize}; -/// State of the contents of the file to download. -/// -/// This is used to represent the state of the source file, as well as the -/// destination file. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum FileDownloadState { - /// File does not exist. - None { - /// Path to the tracked file, if any. - path: Option, - }, - /// String contents of the file. - /// - /// Use this when: - /// - /// * File contents is text. - /// * File is small enough to load in memory. - StringContents { - /// Path to the file. - path: PathBuf, - /// Contents of the file. - contents: String, - }, - /// Length of the file. - /// - /// Use this when: - /// - /// * File is not practical to load in memory. - Length { - /// Path to the file. - path: PathBuf, - /// Number of bytes. - byte_count: u64, - }, - /// Cannot determine file state. - /// - /// May be used for the goal state - Unknown { - /// Path to the file. - path: PathBuf, - }, +use crate::{ETag, FileDownloadStateLogical}; + +#[cfg(feature = "output_progress")] +use peace::item_model::ItemLocationState; + +/// Newtype wrapper for `State>`. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct FileDownloadState(pub State>); + +impl FileDownloadState { + /// Returns a new `FileDownloadState`. + pub fn new( + file_download_state_logical: FileDownloadStateLogical, + etag: FetchedOpt, + ) -> Self { + Self(State::new(file_download_state_logical, etag)) + } +} + +impl From>> for FileDownloadState { + fn from(state: State>) -> Self { + Self(state) + } } impl fmt::Display for FileDownloadState { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::None { path } => { - if let Some(path) = path { - let path = path.display(); - write!(f, "`{path}` non-existent") - } else { - write!(f, "non-existent") - } - } - Self::StringContents { path, contents } => { - let path = path.display(); - write!(f, "`{path}` containing \"{contents}\"") - } - Self::Length { path, byte_count } => { - let path = path.display(); - write!(f, "`{path}` containing {byte_count} bytes") - } - Self::Unknown { path } => { - let path = path.display(); - write!(f, "`{path}` (contents not tracked)") - } - } + self.0.fmt(f) } } -impl PartialEq for FileDownloadState { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - ( - FileDownloadState::None { path: path_self }, - FileDownloadState::None { path: path_other }, - ) => path_self == path_other, - ( - FileDownloadState::Unknown { - path: path_self, .. - }, - FileDownloadState::StringContents { - path: path_other, .. - }, - ) - | ( - FileDownloadState::Unknown { - path: path_self, .. - }, - FileDownloadState::Length { - path: path_other, .. - }, - ) - | ( - FileDownloadState::StringContents { - path: path_self, .. - }, - FileDownloadState::Unknown { - path: path_other, .. - }, - ) - | ( - FileDownloadState::Length { - path: path_self, .. - }, - FileDownloadState::Unknown { - path: path_other, .. - }, - ) - | ( - FileDownloadState::Unknown { path: path_self }, - FileDownloadState::Unknown { path: path_other }, - ) => path_self == path_other, - - (FileDownloadState::Unknown { .. }, FileDownloadState::None { .. }) - | (FileDownloadState::None { .. }, FileDownloadState::Unknown { .. }) - | (FileDownloadState::None { .. }, FileDownloadState::StringContents { .. }) - | (FileDownloadState::StringContents { .. }, FileDownloadState::None { .. }) - | (FileDownloadState::StringContents { .. }, FileDownloadState::Length { .. }) - | (FileDownloadState::Length { .. }, FileDownloadState::None { .. }) - | (FileDownloadState::Length { .. }, FileDownloadState::StringContents { .. }) - | (FileDownloadState::None { .. }, FileDownloadState::Length { .. }) => false, - ( - FileDownloadState::StringContents { - path: path_self, - contents: contents_self, - }, - FileDownloadState::StringContents { - path: path_other, - contents: contents_other, - }, - ) => path_self == path_other && contents_self == contents_other, - ( - FileDownloadState::Length { - path: path_self, - byte_count: byte_count_self, - }, - FileDownloadState::Length { - path: path_other, - byte_count: byte_count_other, - }, - ) => path_self == path_other && byte_count_self == byte_count_other, +#[cfg(feature = "output_progress")] +impl<'state> From<&'state FileDownloadState> for ItemLocationState { + fn from(state: &'state FileDownloadState) -> ItemLocationState { + match &state.0.logical { + FileDownloadStateLogical::None { .. } => ItemLocationState::NotExists, + FileDownloadStateLogical::StringContents { .. } + | FileDownloadStateLogical::Length { .. } + | FileDownloadStateLogical::Unknown { .. } => ItemLocationState::Exists, } } } diff --git a/items/file_download/src/file_download_state_current_fn.rs b/items/file_download/src/file_download_state_current_fn.rs index d3d04b92d..db554b5a0 100644 --- a/items/file_download/src/file_download_state_current_fn.rs +++ b/items/file_download/src/file_download_state_current_fn.rs @@ -1,7 +1,7 @@ use std::{marker::PhantomData, path::Path}; use peace::{ - cfg::{state::FetchedOpt, FnCtx, State}, + cfg::{state::FetchedOpt, FnCtx}, params::Params, }; #[cfg(not(target_arch = "wasm32"))] @@ -10,7 +10,10 @@ use tokio::{fs::File, io::AsyncReadExt}; #[cfg(target_arch = "wasm32")] use peace::rt_model::Storage; -use crate::{ETag, FileDownloadData, FileDownloadError, FileDownloadParams, FileDownloadState}; +use crate::{ + FileDownloadData, FileDownloadError, FileDownloadParams, FileDownloadState, + FileDownloadStateLogical, +}; /// Reads the current state of the file to download. #[derive(Debug)] @@ -24,7 +27,7 @@ where _fn_ctx: FnCtx<'_>, params_partial: & as Params>::Partial, data: FileDownloadData<'_, Id>, - ) -> Result>>, FileDownloadError> { + ) -> Result, FileDownloadError> { if let Some(dest) = params_partial.dest() { Self::state_current_internal(data, dest).await.map(Some) } else { @@ -36,7 +39,7 @@ where _fn_ctx: FnCtx<'_>, params: &FileDownloadParams, data: FileDownloadData<'_, Id>, - ) -> Result>, FileDownloadError> { + ) -> Result { let dest = params.dest(); Self::state_current_internal(data, dest).await @@ -45,14 +48,14 @@ where async fn state_current_internal( data: FileDownloadData<'_, Id>, dest: &Path, - ) -> Result>, FileDownloadError> { + ) -> Result { #[cfg(not(target_arch = "wasm32"))] let file_exists = dest.exists(); #[cfg(target_arch = "wasm32")] let file_exists = data.storage().get_item_opt(dest)?.is_some(); if !file_exists { - return Ok(State::new( - FileDownloadState::None { + return Ok(FileDownloadState::new( + FileDownloadStateLogical::None { path: Some(dest.to_path_buf()), }, FetchedOpt::Tbd, @@ -69,25 +72,25 @@ where let e_tag = data .state_working() .as_ref() - .map(|state_working| state_working.physical.clone()) + .map(|state_working| state_working.0.physical.clone()) .or_else(|| { data.state_prev() .get() - .map(|state_prev| state_prev.physical.clone()) + .map(|state_prev| state_prev.0.physical.clone()) }) - .unwrap_or(if let FileDownloadState::None { .. } = &file_state { + .unwrap_or(if let FileDownloadStateLogical::None { .. } = &file_state { FetchedOpt::Tbd } else { FetchedOpt::None }); - Ok(State::new(file_state, e_tag)) + Ok(FileDownloadState::new(file_state, e_tag)) } #[cfg(not(target_arch = "wasm32"))] async fn read_file_contents( dest: &std::path::Path, - ) -> Result { + ) -> Result { let mut file = File::open(dest) .await .map_err(FileDownloadError::DestFileOpen)?; @@ -96,7 +99,7 @@ where .await .map_err(FileDownloadError::DestMetadataRead)?; let file_state = if metadata.len() > crate::IN_MEMORY_CONTENTS_MAX { - FileDownloadState::Unknown { + FileDownloadStateLogical::Unknown { path: dest.to_path_buf(), } } else { @@ -105,7 +108,7 @@ where file.read_to_string(&mut buffer) .await .map_err(FileDownloadError::DestFileRead)?; - FileDownloadState::StringContents { + FileDownloadStateLogical::StringContents { path: dest.to_path_buf(), contents: buffer, } @@ -117,7 +120,7 @@ where async fn read_file_contents( dest: &std::path::Path, storage: &Storage, - ) -> Result { + ) -> Result { let file_state = storage .get_item_opt(dest)? .map(|contents| { @@ -127,22 +130,22 @@ where .try_into() .map(|byte_count: u64| { if byte_count > crate::IN_MEMORY_CONTENTS_MAX { - FileDownloadState::Unknown { + FileDownloadStateLogical::Unknown { path: dest.to_path_buf(), } } else { - FileDownloadState::StringContents { + FileDownloadStateLogical::StringContents { path: dest.to_path_buf(), contents: contents.clone(), } } }) - .unwrap_or_else(|_| FileDownloadState::StringContents { + .unwrap_or_else(|_| FileDownloadStateLogical::StringContents { path: dest.to_path_buf(), contents: contents.clone(), }) }) - .unwrap_or(FileDownloadState::None { + .unwrap_or(FileDownloadStateLogical::None { path: Some(dest.to_path_buf()), }); diff --git a/items/file_download/src/file_download_state_diff_fn.rs b/items/file_download/src/file_download_state_diff_fn.rs index 2794e4387..465747192 100644 --- a/items/file_download/src/file_download_state_diff_fn.rs +++ b/items/file_download/src/file_download_state_diff_fn.rs @@ -3,7 +3,9 @@ use peace::{ diff::{Changeable, Tracked}, }; -use crate::{ETag, FileDownloadError, FileDownloadState, FileDownloadStateDiff}; +use crate::{ + FileDownloadError, FileDownloadState, FileDownloadStateDiff, FileDownloadStateLogical, +}; /// Download status diff function. #[derive(Debug)] @@ -11,42 +13,42 @@ pub struct FileDownloadStateDiffFn; impl FileDownloadStateDiffFn { pub async fn state_diff( - state_current: &State>, - state_goal: &State>, + state_current: &FileDownloadState, + state_goal: &FileDownloadState, ) -> Result { - let State { + let FileDownloadState(State { logical: file_state_current, physical: e_tag_current, - } = state_current; - let State { + }) = state_current; + let FileDownloadState(State { logical: file_state_goal, physical: e_tag_goal, - } = state_goal; + }) = state_goal; let file_state_diff = { match (file_state_current, file_state_goal) { ( - FileDownloadState::StringContents { path, .. } - | FileDownloadState::Length { path, .. } - | FileDownloadState::Unknown { path, .. }, - FileDownloadState::None { .. }, + FileDownloadStateLogical::StringContents { path, .. } + | FileDownloadStateLogical::Length { path, .. } + | FileDownloadStateLogical::Unknown { path, .. }, + FileDownloadStateLogical::None { .. }, ) => FileDownloadStateDiff::Deleted { path: path.to_path_buf(), }, ( - file_state_current @ (FileDownloadState::StringContents { .. } - | FileDownloadState::Length { .. } - | FileDownloadState::Unknown { .. }), - file_state_goal @ (FileDownloadState::StringContents { path, .. } - | FileDownloadState::Length { path, .. } - | FileDownloadState::Unknown { path, .. }), + file_state_current @ (FileDownloadStateLogical::StringContents { .. } + | FileDownloadStateLogical::Length { .. } + | FileDownloadStateLogical::Unknown { .. }), + file_state_goal @ (FileDownloadStateLogical::StringContents { path, .. } + | FileDownloadStateLogical::Length { path, .. } + | FileDownloadStateLogical::Unknown { path, .. }), ) | ( - file_state_current @ FileDownloadState::None { .. }, - file_state_goal @ (FileDownloadState::StringContents { path, .. } - | FileDownloadState::Length { path, .. } - | FileDownloadState::Unknown { path, .. }), + file_state_current @ FileDownloadStateLogical::None { .. }, + file_state_goal @ (FileDownloadStateLogical::StringContents { path, .. } + | FileDownloadStateLogical::Length { path, .. } + | FileDownloadStateLogical::Unknown { path, .. }), ) => { let path = path.to_path_buf(); let (from_bytes, from_content) = to_file_state_diff(file_state_current); @@ -76,9 +78,10 @@ impl FileDownloadStateDiffFn { (true, true) => FileDownloadStateDiff::NoChangeSync { path }, } } - (FileDownloadState::None { .. }, FileDownloadState::None { path }) => { - FileDownloadStateDiff::NoChangeNotExists { path: path.clone() } - } + ( + FileDownloadStateLogical::None { .. }, + FileDownloadStateLogical::None { path }, + ) => FileDownloadStateDiff::NoChangeNotExists { path: path.clone() }, } }; @@ -86,14 +89,14 @@ impl FileDownloadStateDiffFn { } } -fn to_file_state_diff(file_state: &FileDownloadState) -> (Tracked, Tracked) { +fn to_file_state_diff(file_state: &FileDownloadStateLogical) -> (Tracked, Tracked) { match file_state { - FileDownloadState::None { .. } => (Tracked::None, Tracked::None), - FileDownloadState::StringContents { path: _, contents } => ( + FileDownloadStateLogical::None { .. } => (Tracked::None, Tracked::None), + FileDownloadStateLogical::StringContents { path: _, contents } => ( Tracked::Known(contents.bytes().len()), Tracked::Known(contents.to_owned()), ), - FileDownloadState::Length { + FileDownloadStateLogical::Length { path: _, byte_count, } => ( @@ -103,6 +106,6 @@ fn to_file_state_diff(file_state: &FileDownloadState) -> (Tracked, Tracke .unwrap_or(Tracked::Unknown), Tracked::Unknown, ), - FileDownloadState::Unknown { .. } => (Tracked::Unknown, Tracked::Unknown), + FileDownloadStateLogical::Unknown { .. } => (Tracked::Unknown, Tracked::Unknown), } } diff --git a/items/file_download/src/file_download_state_goal_fn.rs b/items/file_download/src/file_download_state_goal_fn.rs index 8e606924e..46ca106f9 100644 --- a/items/file_download/src/file_download_state_goal_fn.rs +++ b/items/file_download/src/file_download_state_goal_fn.rs @@ -1,12 +1,15 @@ use std::{marker::PhantomData, path::Path}; use peace::{ - cfg::{state::FetchedOpt, FnCtx, State}, + cfg::{state::FetchedOpt, FnCtx}, params::Params, }; use reqwest::{header::ETAG, Url}; -use crate::{ETag, FileDownloadData, FileDownloadError, FileDownloadParams, FileDownloadState}; +use crate::{ + ETag, FileDownloadData, FileDownloadError, FileDownloadParams, FileDownloadState, + FileDownloadStateLogical, +}; /// Reads the goal state of the file to download. #[derive(Debug)] @@ -20,7 +23,7 @@ where _fn_ctx: FnCtx<'_>, params_partial: & as Params>::Partial, data: FileDownloadData<'_, Id>, - ) -> Result>>, FileDownloadError> { + ) -> Result, FileDownloadError> { if let Some((src, dest)) = params_partial.src().zip(params_partial.dest()) { Self::file_state_goal(&data, src, dest).await.map(Some) } else { @@ -32,7 +35,7 @@ where _fn_ctx: FnCtx<'_>, params: &FileDownloadParams, data: FileDownloadData<'_, Id>, - ) -> Result>, FileDownloadError> { + ) -> Result { let file_state_goal = Self::file_state_goal(&data, params.src(), params.dest()).await?; Ok(file_state_goal) @@ -42,7 +45,7 @@ where data: &FileDownloadData<'_, Id>, src_url: &Url, dest: &Path, - ) -> Result>, FileDownloadError> { + ) -> Result { let client = data.client(); let response = client .get(src_url.clone()) @@ -70,24 +73,24 @@ where } .await?; - FileDownloadState::StringContents { + FileDownloadStateLogical::StringContents { path: dest.to_path_buf(), contents: remote_contents, } } else { // Stream it later. - FileDownloadState::Length { + FileDownloadStateLogical::Length { path: dest.to_path_buf(), byte_count: remote_file_length, } } } else { - FileDownloadState::Unknown { + FileDownloadStateLogical::Unknown { path: dest.to_path_buf(), } }; - Ok(State::new(file_download_state, e_tag)) + Ok(FileDownloadState::new(file_download_state, e_tag)) } else { Err(FileDownloadError::SrcFileUndetermined { status_code }) } diff --git a/items/file_download/src/file_download_state_logical.rs b/items/file_download/src/file_download_state_logical.rs new file mode 100644 index 000000000..c9eb76125 --- /dev/null +++ b/items/file_download/src/file_download_state_logical.rs @@ -0,0 +1,178 @@ +use std::{fmt, path::PathBuf}; + +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "output_progress")] +use peace::item_model::ItemLocationState; + +/// State of the contents of the file to download. +/// +/// This is used to represent the state of the source file, as well as the +/// destination file. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum FileDownloadStateLogical { + /// File does not exist. + None { + /// Path to the tracked file, if any. + path: Option, + }, + /// String contents of the file. + /// + /// Use this when: + /// + /// * File contents is text. + /// * File is small enough to load in memory. + StringContents { + /// Path to the file. + path: PathBuf, + /// Contents of the file. + contents: String, + }, + /// Length of the file. + /// + /// Use this when: + /// + /// * File is not practical to load in memory. + Length { + /// Path to the file. + path: PathBuf, + /// Number of bytes. + byte_count: u64, + }, + /// Cannot determine file state. + /// + /// May be used for the goal state + Unknown { + /// Path to the file. + path: PathBuf, + }, +} + +impl fmt::Display for FileDownloadStateLogical { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::None { path } => { + if let Some(path) = path { + let path = path.display(); + write!(f, "`{path}` non-existent") + } else { + write!(f, "non-existent") + } + } + Self::StringContents { path, contents } => { + let path = path.display(); + write!(f, "`{path}` containing \"{contents}\"") + } + Self::Length { path, byte_count } => { + let path = path.display(); + write!(f, "`{path}` containing {byte_count} bytes") + } + Self::Unknown { path } => { + let path = path.display(); + write!(f, "`{path}` (contents not tracked)") + } + } + } +} + +#[cfg(feature = "output_progress")] +impl<'state> From<&'state FileDownloadStateLogical> for ItemLocationState { + fn from(file_download_state: &'state FileDownloadStateLogical) -> ItemLocationState { + match file_download_state { + FileDownloadStateLogical::None { .. } => ItemLocationState::NotExists, + FileDownloadStateLogical::StringContents { .. } + | FileDownloadStateLogical::Length { .. } + | FileDownloadStateLogical::Unknown { .. } => todo!(), + } + } +} + +impl PartialEq for FileDownloadStateLogical { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + ( + FileDownloadStateLogical::None { path: path_self }, + FileDownloadStateLogical::None { path: path_other }, + ) => path_self == path_other, + ( + FileDownloadStateLogical::Unknown { + path: path_self, .. + }, + FileDownloadStateLogical::StringContents { + path: path_other, .. + }, + ) + | ( + FileDownloadStateLogical::Unknown { + path: path_self, .. + }, + FileDownloadStateLogical::Length { + path: path_other, .. + }, + ) + | ( + FileDownloadStateLogical::StringContents { + path: path_self, .. + }, + FileDownloadStateLogical::Unknown { + path: path_other, .. + }, + ) + | ( + FileDownloadStateLogical::Length { + path: path_self, .. + }, + FileDownloadStateLogical::Unknown { + path: path_other, .. + }, + ) + | ( + FileDownloadStateLogical::Unknown { path: path_self }, + FileDownloadStateLogical::Unknown { path: path_other }, + ) => path_self == path_other, + + (FileDownloadStateLogical::Unknown { .. }, FileDownloadStateLogical::None { .. }) + | (FileDownloadStateLogical::None { .. }, FileDownloadStateLogical::Unknown { .. }) + | ( + FileDownloadStateLogical::None { .. }, + FileDownloadStateLogical::StringContents { .. }, + ) + | ( + FileDownloadStateLogical::StringContents { .. }, + FileDownloadStateLogical::None { .. }, + ) + | ( + FileDownloadStateLogical::StringContents { .. }, + FileDownloadStateLogical::Length { .. }, + ) + | (FileDownloadStateLogical::Length { .. }, FileDownloadStateLogical::None { .. }) + | ( + FileDownloadStateLogical::Length { .. }, + FileDownloadStateLogical::StringContents { .. }, + ) + | (FileDownloadStateLogical::None { .. }, FileDownloadStateLogical::Length { .. }) => { + false + } + ( + FileDownloadStateLogical::StringContents { + path: path_self, + contents: contents_self, + }, + FileDownloadStateLogical::StringContents { + path: path_other, + contents: contents_other, + }, + ) => path_self == path_other && contents_self == contents_other, + ( + FileDownloadStateLogical::Length { + path: path_self, + byte_count: byte_count_self, + }, + FileDownloadStateLogical::Length { + path: path_other, + byte_count: byte_count_other, + }, + ) => path_self == path_other && byte_count_self == byte_count_other, + } + } +} diff --git a/items/file_download/src/lib.rs b/items/file_download/src/lib.rs index 184565a51..8f60d2e2e 100644 --- a/items/file_download/src/lib.rs +++ b/items/file_download/src/lib.rs @@ -14,6 +14,7 @@ pub use crate::{ file_download_state_diff::FileDownloadStateDiff, file_download_state_diff_fn::FileDownloadStateDiffFn, file_download_state_goal_fn::FileDownloadStateGoalFn, + file_download_state_logical::FileDownloadStateLogical, }; #[cfg(target_arch = "wasm32")] @@ -30,6 +31,7 @@ mod file_download_state_current_fn; mod file_download_state_diff; mod file_download_state_diff_fn; mod file_download_state_goal_fn; +mod file_download_state_logical; #[cfg(target_arch = "wasm32")] mod storage_form; diff --git a/items/sh_cmd/Cargo.toml b/items/sh_cmd/Cargo.toml index 7d3ab4573..e18f9deba 100644 --- a/items/sh_cmd/Cargo.toml +++ b/items/sh_cmd/Cargo.toml @@ -26,6 +26,7 @@ miette = { workspace = true, optional = true } peace = { workspace = true, default-features = false } serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } +tynm = { workspace = true, optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { workspace = true, features = ["process"] } @@ -37,3 +38,5 @@ tokio = { workspace = true } default = [] error_reporting = ["peace/error_reporting"] output_progress = ["peace/output_progress"] +item_interactions = ["peace/item_interactions"] +item_state_example = ["dep:tynm", "peace/item_state_example"] diff --git a/items/sh_cmd/src/lib.rs b/items/sh_cmd/src/lib.rs index d819e8ab6..72b24e0c3 100644 --- a/items/sh_cmd/src/lib.rs +++ b/items/sh_cmd/src/lib.rs @@ -25,6 +25,7 @@ pub use crate::{ sh_cmd_state::ShCmdState, sh_cmd_state_diff::ShCmdStateDiff, sh_cmd_state_diff_fn::ShCmdStateDiffFn, + sh_cmd_state_logical::ShCmdStateLogical, }; pub(crate) use sh_cmd_executor::ShCmdExecutor; @@ -41,3 +42,4 @@ mod sh_cmd_params; mod sh_cmd_state; mod sh_cmd_state_diff; mod sh_cmd_state_diff_fn; +mod sh_cmd_state_logical; diff --git a/items/sh_cmd/src/sh_cmd.rs b/items/sh_cmd/src/sh_cmd.rs index 04c5f0c75..af459aa16 100644 --- a/items/sh_cmd/src/sh_cmd.rs +++ b/items/sh_cmd/src/sh_cmd.rs @@ -137,6 +137,16 @@ impl From<&ShCmd> for Command { } } +#[cfg(feature = "item_state_example")] +impl From<&ShCmd> for std::process::Command { + fn from(sh_cmd: &ShCmd) -> std::process::Command { + let mut command = std::process::Command::new(&sh_cmd.program); + command.args(&sh_cmd.args); + + command + } +} + impl fmt::Display for ShCmd { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.program.to_string_lossy().fmt(f)?; diff --git a/items/sh_cmd/src/sh_cmd_apply_fns.rs b/items/sh_cmd/src/sh_cmd_apply_fns.rs index ecff9d996..2e79173e4 100644 --- a/items/sh_cmd/src/sh_cmd_apply_fns.rs +++ b/items/sh_cmd/src/sh_cmd_apply_fns.rs @@ -2,11 +2,11 @@ use std::marker::PhantomData; #[cfg(feature = "output_progress")] use peace::cfg::progress::ProgressLimit; -use peace::cfg::{ApplyCheck, FnCtx, State}; +use peace::cfg::{ApplyCheck, FnCtx}; use crate::{ - ShCmd, ShCmdData, ShCmdError, ShCmdExecutionRecord, ShCmdExecutor, ShCmdParams, ShCmdState, - ShCmdStateDiff, + ShCmd, ShCmdData, ShCmdError, ShCmdExecutor, ShCmdParams, ShCmdState, ShCmdStateDiff, + ShCmdStateLogical, }; /// ApplyFns for the command to execute. @@ -20,17 +20,17 @@ where pub async fn apply_check( params: &ShCmdParams, _data: ShCmdData<'_, Id>, - state_current: &State, ShCmdExecutionRecord>, - state_goal: &State, ShCmdExecutionRecord>, + state_current: &ShCmdState, + state_goal: &ShCmdState, state_diff: &ShCmdStateDiff, ) -> Result { - let state_current_arg = match &state_current.logical { - ShCmdState::None => "", - ShCmdState::Some { stdout, .. } => stdout.as_ref(), + let state_current_arg = match &state_current.0.logical { + ShCmdStateLogical::None => "", + ShCmdStateLogical::Some { stdout, .. } => stdout.as_ref(), }; - let state_goal_arg = match &state_goal.logical { - ShCmdState::None => "", - ShCmdState::Some { stdout, .. } => stdout.as_ref(), + let state_goal_arg = match &state_goal.0.logical { + ShCmdStateLogical::None => "", + ShCmdStateLogical::Some { stdout, .. } => stdout.as_ref(), }; let apply_check_sh_cmd = params .apply_check_sh_cmd() @@ -41,8 +41,8 @@ where ShCmdExecutor::::exec(&apply_check_sh_cmd) .await - .and_then(|state| match state.logical { - ShCmdState::Some { stdout, .. } => match stdout.trim().lines().next_back() { + .and_then(|state| match state.0.logical { + ShCmdStateLogical::Some { stdout, .. } => match stdout.trim().lines().next_back() { Some("true") => { #[cfg(not(feature = "output_progress"))] { @@ -75,18 +75,18 @@ where _fn_ctx: FnCtx<'_>, params: &ShCmdParams, _data: ShCmdData<'_, Id>, - state_current: &State, ShCmdExecutionRecord>, - state_goal: &State, ShCmdExecutionRecord>, + state_current: &ShCmdState, + state_goal: &ShCmdState, state_diff: &ShCmdStateDiff, - ) -> Result, ShCmdExecutionRecord>, ShCmdError> { + ) -> Result, ShCmdError> { // TODO: implement properly - let state_current_arg = match &state_current.logical { - ShCmdState::None => "", - ShCmdState::Some { stdout, .. } => stdout.as_ref(), + let state_current_arg = match &state_current.0.logical { + ShCmdStateLogical::None => "", + ShCmdStateLogical::Some { stdout, .. } => stdout.as_ref(), }; - let state_goal_arg = match &state_goal.logical { - ShCmdState::None => "", - ShCmdState::Some { stdout, .. } => stdout.as_ref(), + let state_goal_arg = match &state_goal.0.logical { + ShCmdStateLogical::None => "", + ShCmdStateLogical::Some { stdout, .. } => stdout.as_ref(), }; let apply_exec_sh_cmd = params .apply_exec_sh_cmd() @@ -102,17 +102,17 @@ where _fn_ctx: FnCtx<'_>, params: &ShCmdParams, _data: ShCmdData<'_, Id>, - state_current: &State, ShCmdExecutionRecord>, - state_goal: &State, ShCmdExecutionRecord>, + state_current: &ShCmdState, + state_goal: &ShCmdState, state_diff: &ShCmdStateDiff, - ) -> Result, ShCmdExecutionRecord>, ShCmdError> { - let state_current_arg = match &state_current.logical { - ShCmdState::None => "", - ShCmdState::Some { stdout, .. } => stdout.as_ref(), + ) -> Result, ShCmdError> { + let state_current_arg = match &state_current.0.logical { + ShCmdStateLogical::None => "", + ShCmdStateLogical::Some { stdout, .. } => stdout.as_ref(), }; - let state_goal_arg = match &state_goal.logical { - ShCmdState::None => "", - ShCmdState::Some { stdout, .. } => stdout.as_ref(), + let state_goal_arg = match &state_goal.0.logical { + ShCmdStateLogical::None => "", + ShCmdStateLogical::Some { stdout, .. } => stdout.as_ref(), }; let apply_exec_sh_cmd = params .apply_exec_sh_cmd() diff --git a/items/sh_cmd/src/sh_cmd_data.rs b/items/sh_cmd/src/sh_cmd_data.rs index 848bd8e77..78721596d 100644 --- a/items/sh_cmd/src/sh_cmd_data.rs +++ b/items/sh_cmd/src/sh_cmd_data.rs @@ -5,7 +5,7 @@ use peace::{ data::Data, }; -use crate::{ShCmdExecutionRecord, ShCmdState}; +use crate::{ShCmdExecutionRecord, ShCmdStateLogical}; /// Data used to run a shell command. /// @@ -19,18 +19,20 @@ where Id: Send + Sync + 'static, { /// Stored states of this item's previous execution. - state_current_stored: Stored<'exec, State, ShCmdExecutionRecord>>, + state_current_stored: Stored<'exec, State, ShCmdExecutionRecord>>, /// Marker. marker: PhantomData, } -impl<'exec, Id> ShCmdData<'exec, Id> +impl ShCmdData<'_, Id> where Id: Send + Sync + 'static, { /// Returns the previous states. - pub fn state_current_stored(&self) -> Option<&State, ShCmdExecutionRecord>> { + pub fn state_current_stored( + &self, + ) -> Option<&State, ShCmdExecutionRecord>> { self.state_current_stored.get() } } diff --git a/items/sh_cmd/src/sh_cmd_executor.rs b/items/sh_cmd/src/sh_cmd_executor.rs index c40554625..d608c4fd9 100644 --- a/items/sh_cmd/src/sh_cmd_executor.rs +++ b/items/sh_cmd/src/sh_cmd_executor.rs @@ -1,10 +1,9 @@ use std::{marker::PhantomData, process::Stdio}; use chrono::Utc; -use peace::cfg::State; use tokio::process::Command; -use crate::{ShCmd, ShCmdError, ShCmdExecutionRecord, ShCmdState}; +use crate::{ShCmd, ShCmdError, ShCmdExecutionRecord, ShCmdState, ShCmdStateLogical}; /// Common code to run `ShCmd`s. #[derive(Debug)] @@ -12,9 +11,7 @@ pub(crate) struct ShCmdExecutor(PhantomData); impl ShCmdExecutor { /// Executes the provided `ShCmd` and returns execution information. - pub async fn exec( - sh_cmd: &ShCmd, - ) -> Result, ShCmdExecutionRecord>, ShCmdError> { + pub async fn exec(sh_cmd: &ShCmd) -> Result, ShCmdError> { let start_datetime = Utc::now(); let mut command: Command = sh_cmd.into(); let output = command @@ -76,8 +73,81 @@ impl ShCmdExecutor { .trim() .to_string(); - Ok(State::new( - ShCmdState::Some { + Ok(ShCmdState::new( + ShCmdStateLogical::Some { + stdout, + stderr, + marker: PhantomData, + }, + ShCmdExecutionRecord::Some { + start_datetime, + end_datetime, + exit_code: output.status.code(), + }, + )) + } + + /// Executes the provided `ShCmd` and returns execution information. + #[cfg(feature = "item_state_example")] + pub fn exec_blocking(sh_cmd: &ShCmd) -> Result, ShCmdError> { + let start_datetime = Utc::now(); + let mut command: std::process::Command = sh_cmd.into(); + let output = command.stdin(Stdio::null()).output().map_err(|error| { + #[cfg(feature = "error_reporting")] + let sh_cmd_string = format!("{sh_cmd}"); + + ShCmdError::CmdExecFail { + sh_cmd: sh_cmd.clone(), + #[cfg(feature = "error_reporting")] + sh_cmd_string, + error, + } + })?; + let end_datetime = Utc::now(); + + let stdout = String::from_utf8(output.stdout).map_err(|from_utf8_error| { + let stdout_lossy = String::from_utf8_lossy(from_utf8_error.as_bytes()).to_string(); + let error = from_utf8_error.utf8_error(); + #[cfg(feature = "error_reporting")] + let invalid_span = { + let start = error.valid_up_to(); + let len = error.error_len().unwrap_or(1); + peace::miette::SourceSpan::from((start, len)) + }; + + ShCmdError::StdoutNonUtf8 { + sh_cmd: sh_cmd.clone(), + stdout_lossy, + #[cfg(feature = "error_reporting")] + invalid_span, + error, + } + })?; + + let stderr = String::from_utf8(output.stderr) + .map_err(|from_utf8_error| { + let stderr_lossy = String::from_utf8_lossy(from_utf8_error.as_bytes()).to_string(); + let error = from_utf8_error.utf8_error(); + #[cfg(feature = "error_reporting")] + let invalid_span = { + let start = error.valid_up_to(); + let len = error.error_len().unwrap_or(1); + peace::miette::SourceSpan::from((start, len)) + }; + + ShCmdError::StderrNonUtf8 { + sh_cmd: sh_cmd.clone(), + stderr_lossy, + #[cfg(feature = "error_reporting")] + invalid_span, + error, + } + })? + .trim() + .to_string(); + + Ok(ShCmdState::new( + ShCmdStateLogical::Some { stdout, stderr, marker: PhantomData, diff --git a/items/sh_cmd/src/sh_cmd_item.rs b/items/sh_cmd/src/sh_cmd_item.rs index 2cca486e2..5b33d2166 100644 --- a/items/sh_cmd/src/sh_cmd_item.rs +++ b/items/sh_cmd/src/sh_cmd_item.rs @@ -1,14 +1,14 @@ use std::marker::PhantomData; use peace::{ - cfg::{async_trait, ApplyCheck, FnCtx, Item, ItemId, State}, + cfg::{async_trait, ApplyCheck, FnCtx, Item, ItemId}, params::Params, resource_rt::{resources::ts::Empty, Resources}, }; use crate::{ - ShCmdApplyFns, ShCmdData, ShCmdError, ShCmdExecutionRecord, ShCmdExecutor, ShCmdParams, - ShCmdState, ShCmdStateDiff, ShCmdStateDiffFn, + ShCmdApplyFns, ShCmdData, ShCmdError, ShCmdExecutor, ShCmdParams, ShCmdState, ShCmdStateDiff, + ShCmdStateDiffFn, }; /// Item for executing a shell command. @@ -59,7 +59,7 @@ where type Data<'exec> = ShCmdData<'exec, Id>; type Error = ShCmdError; type Params<'exec> = ShCmdParams; - type State = State, ShCmdExecutionRecord>; + type State = ShCmdState; type StateDiff = ShCmdStateDiff; fn id(&self) -> &ItemId { @@ -70,6 +70,13 @@ where Ok(()) } + #[cfg(feature = "item_state_example")] + fn state_example(params: &Self::Params<'_>, _data: Self::Data<'_>) -> Self::State { + let state_example_sh_cmd = params.state_example_sh_cmd(); + ShCmdExecutor::exec_blocking(state_example_sh_cmd) + .expect("ShCmd failed to return example state.") + } + async fn try_state_current( _fn_ctx: FnCtx<'_>, params_partial: & as Params>::Partial, @@ -176,4 +183,17 @@ where ) -> Result { ShCmdApplyFns::::apply(fn_ctx, params, data, state_current, state_target, diff).await } + + #[cfg(feature = "item_interactions")] + fn interactions( + _params: &Self::Params<'_>, + _data: Self::Data<'_>, + ) -> Vec { + use peace::item_model::{ItemInteractionWithin, ItemLocation}; + + let item_interaction = + ItemInteractionWithin::new(vec![ItemLocation::localhost()].into()).into(); + + vec![item_interaction] + } } diff --git a/items/sh_cmd/src/sh_cmd_params.rs b/items/sh_cmd/src/sh_cmd_params.rs index 319e71444..e9fef71fc 100644 --- a/items/sh_cmd/src/sh_cmd_params.rs +++ b/items/sh_cmd/src/sh_cmd_params.rs @@ -19,6 +19,14 @@ use crate::ShCmd; #[derivative(Clone, Debug)] #[serde(bound = "")] pub struct ShCmdParams { + /// Shell command to run to discover the example state. + /// + /// The command's stdout is used as the example state. + /// + /// The command's stderr is used as the human readable description of the + /// state. This must be output as a single line. + #[cfg(feature = "item_state_example")] + state_example_sh_cmd: ShCmd, /// Shell command to run to discover the clean state. /// /// The command's stdout is used as the clean state. @@ -82,6 +90,7 @@ impl ShCmdParams { /// Returns new `ShCmdParams`. #[allow(clippy::too_many_arguments)] pub fn new( + #[cfg(feature = "item_state_example")] state_example_sh_cmd: ShCmd, state_clean_sh_cmd: ShCmd, state_current_sh_cmd: ShCmd, state_goal_sh_cmd: ShCmd, @@ -90,6 +99,8 @@ impl ShCmdParams { apply_exec_sh_cmd: ShCmd, ) -> Self { Self { + #[cfg(feature = "item_state_example")] + state_example_sh_cmd, state_clean_sh_cmd, state_current_sh_cmd, state_goal_sh_cmd, @@ -100,6 +111,17 @@ impl ShCmdParams { } } + /// Returns the shell command to run to discover the example state. + /// + /// The command's stdout is used as the example state. + /// + /// The command's stderr is used as the human readable description of the + /// state. This must be output as a single line. + #[cfg(feature = "item_state_example")] + pub fn state_example_sh_cmd(&self) -> &ShCmd { + &self.state_example_sh_cmd + } + /// Returns the shell command to run to discover the clean state. /// /// The command's stdout is used as the clean state. diff --git a/items/sh_cmd/src/sh_cmd_state.rs b/items/sh_cmd/src/sh_cmd_state.rs index 3b1bdd5c7..9554775ee 100644 --- a/items/sh_cmd/src/sh_cmd_state.rs +++ b/items/sh_cmd/src/sh_cmd_state.rs @@ -1,36 +1,48 @@ -use std::{fmt, marker::PhantomData}; +use std::fmt; use derivative::Derivative; +use peace::cfg::State; use serde::{Deserialize, Serialize}; -/// State of the shell command execution. -/// -/// * If the command has never been executed, this will be `None`. -/// * If it has been executed, this is `Some(String)` captured from stdout. -#[derive(Derivative, Serialize, Deserialize, Eq)] -#[derivative(Clone, Debug, PartialEq)] -pub enum ShCmdState { - /// The command is not executed. - /// - /// Represents when the command has either never been executed, or has been - /// cleaned up. - None, - /// Command has not been executed since the source files have been updated. - Some { - /// stdout output. - stdout: String, - /// stderr output. - stderr: String, - /// Marker. - marker: PhantomData, - }, +use crate::{ShCmdExecutionRecord, ShCmdStateLogical}; + +#[cfg(feature = "output_progress")] +use peace::item_model::ItemLocationState; + +/// Newtype wrapper for `State, ShCmdExecutionRecord>`. +#[derive(Derivative, Serialize, Deserialize)] +#[derivative(Clone(bound = ""), Debug(bound = ""), PartialEq(bound = ""))] +#[serde(bound(serialize = "", deserialize = ""))] +pub struct ShCmdState(pub State, ShCmdExecutionRecord>); + +impl ShCmdState { + /// Returns a new `ShCmdState`. + pub fn new( + sh_cmd_state_physical: ShCmdStateLogical, + execution_record: ShCmdExecutionRecord, + ) -> Self { + Self(State::new(sh_cmd_state_physical, execution_record)) + } +} + +impl From, ShCmdExecutionRecord>> for ShCmdState { + fn from(state: State, ShCmdExecutionRecord>) -> Self { + Self(state) + } } impl fmt::Display for ShCmdState { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::None => write!(f, ""), - Self::Some { stderr, .. } => stderr.fmt(f), + self.0.fmt(f) + } +} + +#[cfg(feature = "output_progress")] +impl<'state, Id> From<&'state ShCmdState> for ItemLocationState { + fn from(state: &'state ShCmdState) -> ItemLocationState { + match &state.0.logical { + ShCmdStateLogical::Some { .. } => ItemLocationState::Exists, + ShCmdStateLogical::None => ItemLocationState::NotExists, } } } diff --git a/items/sh_cmd/src/sh_cmd_state_diff_fn.rs b/items/sh_cmd/src/sh_cmd_state_diff_fn.rs index 18c463b3b..253017f79 100644 --- a/items/sh_cmd/src/sh_cmd_state_diff_fn.rs +++ b/items/sh_cmd/src/sh_cmd_state_diff_fn.rs @@ -1,8 +1,6 @@ use std::marker::PhantomData; -use peace::cfg::State; - -use crate::{ShCmd, ShCmdError, ShCmdExecutionRecord, ShCmdExecutor, ShCmdState, ShCmdStateDiff}; +use crate::{ShCmd, ShCmdError, ShCmdExecutor, ShCmdState, ShCmdStateDiff, ShCmdStateLogical}; /// Runs a shell command to obtain the `ShCmd` diff. #[derive(Debug)] @@ -14,23 +12,23 @@ where { pub async fn state_diff( state_diff_sh_cmd: ShCmd, - state_current: &State, ShCmdExecutionRecord>, - state_goal: &State, ShCmdExecutionRecord>, + state_current: &ShCmdState, + state_goal: &ShCmdState, ) -> Result { - let state_current_arg = match &state_current.logical { - ShCmdState::None => "", - ShCmdState::Some { stdout, .. } => stdout.as_ref(), + let state_current_arg = match &state_current.0.logical { + ShCmdStateLogical::None => "", + ShCmdStateLogical::Some { stdout, .. } => stdout.as_ref(), }; - let state_goal_arg = match &state_goal.logical { - ShCmdState::None => "", - ShCmdState::Some { stdout, .. } => stdout.as_ref(), + let state_goal_arg = match &state_goal.0.logical { + ShCmdStateLogical::None => "", + ShCmdStateLogical::Some { stdout, .. } => stdout.as_ref(), }; let state_diff_sh_cmd = state_diff_sh_cmd.arg(state_current_arg).arg(state_goal_arg); ShCmdExecutor::::exec(&state_diff_sh_cmd) .await - .map(|state| match state.logical { - ShCmdState::None => ShCmdStateDiff::new(String::from(""), String::from("")), - ShCmdState::Some { + .map(|state| match state.0.logical { + ShCmdStateLogical::None => ShCmdStateDiff::new(String::from(""), String::from("")), + ShCmdStateLogical::Some { stdout, stderr, marker: _, diff --git a/items/sh_cmd/src/sh_cmd_state_logical.rs b/items/sh_cmd/src/sh_cmd_state_logical.rs new file mode 100644 index 000000000..2e79bc07b --- /dev/null +++ b/items/sh_cmd/src/sh_cmd_state_logical.rs @@ -0,0 +1,49 @@ +use std::{fmt, marker::PhantomData}; + +use derivative::Derivative; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "output_progress")] +use peace::item_model::ItemLocationState; + +/// State of the shell command execution. +/// +/// * If the command has never been executed, this will be `None`. +/// * If it has been executed, this is `Some(String)` captured from stdout. +#[derive(Derivative, Serialize, Deserialize, Eq)] +#[derivative(Clone, Debug, PartialEq)] +pub enum ShCmdStateLogical { + /// The command is not executed. + /// + /// Represents when the command has either never been executed, or has been + /// cleaned up. + None, + /// Command has not been executed since the source files have been updated. + Some { + /// stdout output. + stdout: String, + /// stderr output. + stderr: String, + /// Marker. + marker: PhantomData, + }, +} + +impl fmt::Display for ShCmdStateLogical { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::None => write!(f, ""), + Self::Some { stderr, .. } => stderr.fmt(f), + } + } +} + +#[cfg(feature = "output_progress")] +impl<'state, Id> From<&'state ShCmdStateLogical> for ItemLocationState { + fn from(sh_cmd_state: &'state ShCmdStateLogical) -> ItemLocationState { + match sh_cmd_state { + ShCmdStateLogical::Some { .. } => ItemLocationState::Exists, + ShCmdStateLogical::None => ItemLocationState::NotExists, + } + } +} diff --git a/items/tar_x/Cargo.toml b/items/tar_x/Cargo.toml index da6e95a4c..beeaf25e4 100644 --- a/items/tar_x/Cargo.toml +++ b/items/tar_x/Cargo.toml @@ -47,3 +47,5 @@ tokio = { workspace = true } default = [] error_reporting = ["peace/error_reporting"] output_progress = ["peace/output_progress"] +item_interactions = ["peace/item_interactions"] +item_state_example = ["peace/item_state_example"] diff --git a/items/tar_x/src/file_metadatas.rs b/items/tar_x/src/file_metadatas.rs index 3ad4afb32..7b158c27b 100644 --- a/items/tar_x/src/file_metadatas.rs +++ b/items/tar_x/src/file_metadatas.rs @@ -4,6 +4,9 @@ use serde::{Deserialize, Serialize}; use crate::FileMetadata; +#[cfg(feature = "output_progress")] +use peace::item_model::ItemLocationState; + /// Metadata of files to extract. /// /// The `FileMetadata`s are sorted by their path. @@ -49,3 +52,13 @@ impl From> for FileMetadatas { Self(file_metadatas) } } + +#[cfg(feature = "output_progress")] +impl<'state> From<&'state FileMetadatas> for ItemLocationState { + fn from(file_metadatas: &'state FileMetadatas) -> ItemLocationState { + match file_metadatas.0.is_empty() { + true => ItemLocationState::NotExists, + false => ItemLocationState::Exists, + } + } +} diff --git a/items/tar_x/src/tar_x_data.rs b/items/tar_x/src/tar_x_data.rs index 2152e40f3..3322f19c5 100644 --- a/items/tar_x/src/tar_x_data.rs +++ b/items/tar_x/src/tar_x_data.rs @@ -23,7 +23,7 @@ where marker: PhantomData, } -impl<'exec, Id> TarXData<'exec, Id> +impl TarXData<'_, Id> where Id: Send + Sync + 'static, { diff --git a/items/tar_x/src/tar_x_item.rs b/items/tar_x/src/tar_x_item.rs index cd6681e98..8842ecbd7 100644 --- a/items/tar_x/src/tar_x_item.rs +++ b/items/tar_x/src/tar_x_item.rs @@ -72,6 +72,28 @@ where Ok(()) } + #[cfg(feature = "item_state_example")] + fn state_example(_params: &Self::Params<'_>, _data: Self::Data<'_>) -> Self::State { + use std::{ + path::PathBuf, + time::{Duration, SystemTime, UNIX_EPOCH}, + }; + + use crate::FileMetadata; + + let mtime = SystemTime::now() + .duration_since(UNIX_EPOCH) + .as_ref() + .map(Duration::as_secs) + .unwrap_or(0u64); + let files_extracted = vec![ + FileMetadata::new(PathBuf::from(String::from("tar_x_example_1.txt")), mtime), + FileMetadata::new(PathBuf::from(String::from("tar_x_example_2.txt")), mtime), + ]; + + FileMetadatas::from(files_extracted) + } + async fn try_state_current( fn_ctx: FnCtx<'_>, params_partial: & as Params>::Partial, @@ -151,4 +173,21 @@ where ) -> Result { TarXApplyFns::::apply(fn_ctx, params, data, state_current, state_target, diff).await } + + #[cfg(feature = "item_interactions")] + fn interactions( + params: &Self::Params<'_>, + _data: Self::Data<'_>, + ) -> Vec { + use peace::item_model::{ItemInteractionWithin, ItemLocation, ItemLocationAncestors}; + + let location: ItemLocationAncestors = vec![ + ItemLocation::localhost(), + ItemLocation::path(format!("📁 {}", params.dest().display())), + ] + .into(); + let item_interaction = ItemInteractionWithin::new(location).into(); + + vec![item_interaction] + } } diff --git a/rustfmt.toml b/rustfmt.toml index b7e1fa537..e1a63c6e4 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,7 +1,7 @@ +edition = "2021" +format_code_in_doc_comments = true imports_granularity = 'crate' reorder_impl_items = true +style_edition = "2021" use_field_init_shorthand = true -format_code_in_doc_comments = true wrap_comments = true -edition = "2021" -version = "Two" diff --git a/src/lib.rs b/src/lib.rs index 45ca5bc1e..92ce34e81 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,8 @@ 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; +#[cfg(feature = "item_interactions")] +pub use peace_item_model as item_model; pub use peace_params as params; pub use peace_resource_rt as resource_rt; pub use peace_rt as rt; diff --git a/workspace_tests/Cargo.toml b/workspace_tests/Cargo.toml index ed2f785bd..c03033a46 100644 --- a/workspace_tests/Cargo.toml +++ b/workspace_tests/Cargo.toml @@ -46,6 +46,8 @@ default = ["items", "output_in_memory", "webi"] error_reporting = ["peace/error_reporting"] output_in_memory = ["peace/output_in_memory"] output_progress = ["peace/output_progress", "peace_items/output_progress"] +item_interactions = ["peace/item_interactions", "peace_items/item_interactions"] +item_state_example = ["peace/item_state_example", "peace_items/item_state_example"] webi = ["peace/webi"] # `peace_items` features diff --git a/workspace_tests/src/cfg/progress/progress_sender.rs b/workspace_tests/src/cfg/progress/progress_sender.rs index dc3c5a785..bfba72665 100644 --- a/workspace_tests/src/cfg/progress/progress_sender.rs +++ b/workspace_tests/src/cfg/progress/progress_sender.rs @@ -22,7 +22,7 @@ fn clone() { let cmd_progress_update = progress_rx.try_recv().unwrap(); assert_eq!( - CmdProgressUpdate::Item { + CmdProgressUpdate::ItemProgress { progress_update_and_id: ProgressUpdateAndId { item_id: item_id!("test_item_id"), progress_update: ProgressUpdate::Delta(ProgressDelta::Inc(123)), @@ -46,7 +46,7 @@ fn inc_sends_progress_update() -> Result<(), Box> { let cmd_progress_update = progress_rx.try_recv().unwrap(); assert_eq!( - CmdProgressUpdate::Item { + CmdProgressUpdate::ItemProgress { progress_update_and_id: ProgressUpdateAndId { item_id: item_id!("test_item_id"), progress_update: ProgressUpdate::Delta(ProgressDelta::Inc(123)), @@ -61,8 +61,8 @@ fn inc_sends_progress_update() -> Result<(), Box> { } #[test] -fn inc_is_received_if_sent_before_progress_channel_is_closed() --> Result<(), Box> { +fn inc_is_received_if_sent_before_progress_channel_is_closed( +) -> Result<(), Box> { let item_id = item_id!("test_item_id"); let (progress_tx, mut progress_rx) = mpsc::channel(10); let progress_sender = ProgressSender::new(&item_id, &progress_tx); @@ -73,7 +73,7 @@ fn inc_is_received_if_sent_before_progress_channel_is_closed() let cmd_progress_update = progress_rx.try_recv().unwrap(); assert_eq!( - CmdProgressUpdate::Item { + CmdProgressUpdate::ItemProgress { progress_update_and_id: ProgressUpdateAndId { item_id: item_id!("test_item_id"), progress_update: ProgressUpdate::Delta(ProgressDelta::Inc(123)), @@ -112,7 +112,7 @@ fn tick_sends_progress_update() -> Result<(), Box> { let cmd_progress_update = progress_rx.try_recv().unwrap(); assert_eq!( - CmdProgressUpdate::Item { + CmdProgressUpdate::ItemProgress { progress_update_and_id: ProgressUpdateAndId { item_id: item_id!("test_item_id"), progress_update: ProgressUpdate::Delta(ProgressDelta::Tick), @@ -127,8 +127,8 @@ fn tick_sends_progress_update() -> Result<(), Box> { } #[test] -fn tick_is_received_if_sent_before_progress_channel_is_closed() --> Result<(), Box> { +fn tick_is_received_if_sent_before_progress_channel_is_closed( +) -> Result<(), Box> { let item_id = item_id!("test_item_id"); let (progress_tx, mut progress_rx) = mpsc::channel(10); let progress_sender = ProgressSender::new(&item_id, &progress_tx); @@ -139,7 +139,7 @@ fn tick_is_received_if_sent_before_progress_channel_is_closed() let cmd_progress_update = progress_rx.try_recv().unwrap(); assert_eq!( - CmdProgressUpdate::Item { + CmdProgressUpdate::ItemProgress { progress_update_and_id: ProgressUpdateAndId { item_id: item_id!("test_item_id"), progress_update: ProgressUpdate::Delta(ProgressDelta::Tick), @@ -173,9 +173,6 @@ fn debug() { let (progress_tx, _progress_rx) = mpsc::channel(10); let progress_sender = ProgressSender::new(&item_id, &progress_tx); - assert!( - format!("{progress_sender:?}").starts_with( - r#"ProgressSender { item_id: ItemId("test_item_id"), progress_tx: Sender"# - ) - ); + assert!(format!("{progress_sender:?}") + .starts_with(r#"ProgressSender { item_id: ItemId("test_item_id"), progress_tx: Sender"#)); } diff --git a/workspace_tests/src/cli/output/cli_md_presenter.rs b/workspace_tests/src/cli/output/cli_md_presenter.rs index 18517614b..8b3cf92b3 100644 --- a/workspace_tests/src/cli/output/cli_md_presenter.rs +++ b/workspace_tests/src/cli/output/cli_md_presenter.rs @@ -90,8 +90,8 @@ async fn presents_heading_with_hashes_color_enabled() -> Result<(), Box Result<(), Box> { +async fn presents_bold_with_double_asterisk_color_disabled( +) -> Result<(), Box> { let mut buffer = Vec::new(); let mut cli_output = cli_output(&mut buffer, CliColorizeOpt::Never); let mut presenter = CliMdPresenter::new(&mut cli_output); @@ -120,8 +120,8 @@ async fn presents_bold_with_double_asterisk_color_enabled() -> Result<(), Box Result<(), Box> { +async fn presents_bold_wrapping_code_inline_with_double_asterisk_color_disabled( +) -> Result<(), Box> { let mut buffer = Vec::new(); let mut cli_output = cli_output(&mut buffer, CliColorizeOpt::Never); let mut presenter = CliMdPresenter::new(&mut cli_output); @@ -135,8 +135,8 @@ async fn presents_bold_wrapping_code_inline_with_double_asterisk_color_disabled( } #[tokio::test] -async fn presents_bold_wrapping_code_inline_with_double_asterisk_color_enabled() --> Result<(), Box> { +async fn presents_bold_wrapping_code_inline_with_double_asterisk_color_enabled( +) -> Result<(), Box> { let mut buffer = Vec::new(); let mut cli_output = cli_output(&mut buffer, CliColorizeOpt::Always); let mut presenter = CliMdPresenter::new(&mut cli_output); @@ -177,8 +177,8 @@ async fn presents_id_as_blue_text_color_enabled() -> Result<(), Box Result<(), Box> { +async fn presents_name_with_double_asterisk_color_disabled( +) -> Result<(), Box> { let mut buffer = Vec::new(); let mut cli_output = cli_output(&mut buffer, CliColorizeOpt::Never); let mut presenter = CliMdPresenter::new(&mut cli_output); @@ -191,8 +191,8 @@ async fn presents_name_with_double_asterisk_color_disabled() } #[tokio::test] -async fn presents_name_with_double_asterisk_bold_text_color_enabled() --> Result<(), Box> { +async fn presents_name_with_double_asterisk_bold_text_color_enabled( +) -> Result<(), Box> { let mut buffer = Vec::new(); let mut cli_output = cli_output(&mut buffer, CliColorizeOpt::Always); let mut presenter = CliMdPresenter::new(&mut cli_output); @@ -232,8 +232,8 @@ async fn presents_text_as_plain_text_color_enabled() -> Result<(), Box Result<(), Box> { +async fn presents_tag_with_black_tortoise_shell_plain_text_color_disabled( +) -> Result<(), Box> { let mut buffer = Vec::new(); let mut cli_output = cli_output(&mut buffer, CliColorizeOpt::Never); let mut presenter = CliMdPresenter::new(&mut cli_output); @@ -246,8 +246,8 @@ async fn presents_tag_with_black_tortoise_shell_plain_text_color_disabled() } #[tokio::test] -async fn presents_tag_with_black_tortoise_shell_purple_text_color_enabled() --> Result<(), Box> { +async fn presents_tag_with_black_tortoise_shell_purple_text_color_enabled( +) -> Result<(), Box> { let mut buffer = Vec::new(); let mut cli_output = cli_output(&mut buffer, CliColorizeOpt::Always); let mut presenter = CliMdPresenter::new(&mut cli_output); @@ -261,8 +261,8 @@ async fn presents_tag_with_black_tortoise_shell_purple_text_color_enabled() } #[tokio::test] -async fn presents_code_inline_with_backticks_color_disabled() --> Result<(), Box> { +async fn presents_code_inline_with_backticks_color_disabled( +) -> Result<(), Box> { let mut buffer = Vec::new(); let mut cli_output = cli_output(&mut buffer, CliColorizeOpt::Never); let mut presenter = CliMdPresenter::new(&mut cli_output); @@ -275,8 +275,8 @@ async fn presents_code_inline_with_backticks_color_disabled() } #[tokio::test] -async fn presents_code_inline_with_backticks_blue_text_color_enabled() --> Result<(), Box> { +async fn presents_code_inline_with_backticks_blue_text_color_enabled( +) -> Result<(), Box> { let mut buffer = Vec::new(); let mut cli_output = cli_output(&mut buffer, CliColorizeOpt::Always); let mut presenter = CliMdPresenter::new(&mut cli_output); @@ -448,8 +448,8 @@ async fn presents_list_numbered_with_color_disabled() -> Result<(), Box Result<(), Box> { +async fn presents_list_numbered_with_white_text_color_enabled( +) -> Result<(), Box> { let mut buffer = Vec::new(); let mut cli_output = cli_output(&mut buffer, CliColorizeOpt::Always); let mut presenter = CliMdPresenter::new(&mut cli_output); @@ -483,8 +483,8 @@ async fn presents_list_numbered_with_white_text_color_enabled() } #[tokio::test] -async fn presents_list_numbered_with_padding_color_disabled() --> Result<(), Box> { +async fn presents_list_numbered_with_padding_color_disabled( +) -> Result<(), Box> { let mut buffer = Vec::new(); let mut cli_output = cli_output(&mut buffer, CliColorizeOpt::Never); let mut presenter = CliMdPresenter::new(&mut cli_output); @@ -513,8 +513,8 @@ async fn presents_list_numbered_with_padding_color_disabled() } #[tokio::test] -async fn presents_list_numbered_with_padding_color_enabled() --> Result<(), Box> { +async fn presents_list_numbered_with_padding_color_enabled( +) -> Result<(), Box> { let mut buffer = Vec::new(); let mut cli_output = cli_output(&mut buffer, CliColorizeOpt::Always); let mut presenter = CliMdPresenter::new(&mut cli_output); @@ -594,8 +594,8 @@ async fn presents_list_numbered_aligned_color_disabled() -> Result<(), Box Result<(), Box> { +async fn presents_list_numbered_aligned_white_text_color_enabled( +) -> Result<(), Box> { let mut buffer = Vec::new(); let mut cli_output = cli_output(&mut buffer, CliColorizeOpt::Always); let mut presenter = CliMdPresenter::new(&mut cli_output); @@ -638,8 +638,8 @@ async fn presents_list_numbered_aligned_white_text_color_enabled() } #[tokio::test] -async fn presents_list_numbered_aligned_padding_color_disabled() --> Result<(), Box> { +async fn presents_list_numbered_aligned_padding_color_disabled( +) -> Result<(), Box> { let mut buffer = Vec::new(); let mut cli_output = cli_output(&mut buffer, CliColorizeOpt::Never); let mut presenter = CliMdPresenter::new(&mut cli_output); @@ -680,8 +680,8 @@ async fn presents_list_numbered_aligned_padding_color_disabled() } #[tokio::test] -async fn presents_list_numbered_aligned_padding_color_enabled() --> Result<(), Box> { +async fn presents_list_numbered_aligned_padding_color_enabled( +) -> Result<(), Box> { let mut buffer = Vec::new(); let mut cli_output = cli_output(&mut buffer, CliColorizeOpt::Always); let mut presenter = CliMdPresenter::new(&mut cli_output); @@ -814,8 +814,8 @@ async fn presents_list_bulleted_with_color_disabled() -> Result<(), Box Result<(), Box> { +async fn presents_list_bulleted_with_white_text_color_enabled( +) -> Result<(), Box> { let mut buffer = Vec::new(); let mut cli_output = cli_output(&mut buffer, CliColorizeOpt::Always); let mut presenter = CliMdPresenter::new(&mut cli_output); @@ -885,8 +885,8 @@ async fn presents_list_bulleted_aligned_color_disabled() -> Result<(), Box Result<(), Box> { +async fn presents_list_bulleted_aligned_white_text_color_enabled( +) -> Result<(), Box> { let mut buffer = Vec::new(); let mut cli_output = cli_output(&mut buffer, CliColorizeOpt::Always); let mut presenter = CliMdPresenter::new(&mut cli_output); @@ -929,8 +929,8 @@ async fn presents_list_bulleted_aligned_white_text_color_enabled() } #[tokio::test] -async fn presents_list_bulleted_aligned_padding_color_disabled() --> Result<(), Box> { +async fn presents_list_bulleted_aligned_padding_color_disabled( +) -> Result<(), Box> { let mut buffer = Vec::new(); let mut cli_output = cli_output(&mut buffer, CliColorizeOpt::Never); let mut presenter = CliMdPresenter::new(&mut cli_output); @@ -971,8 +971,8 @@ async fn presents_list_bulleted_aligned_padding_color_disabled() } #[tokio::test] -async fn presents_list_bulleted_aligned_padding_color_enabled() --> Result<(), Box> { +async fn presents_list_bulleted_aligned_padding_color_enabled( +) -> Result<(), Box> { let mut buffer = Vec::new(); let mut cli_output = cli_output(&mut buffer, CliColorizeOpt::Always); let mut presenter = CliMdPresenter::new(&mut cli_output); diff --git a/workspace_tests/src/cli/output/cli_output_builder.rs b/workspace_tests/src/cli/output/cli_output_builder.rs index f17986856..87d3e38fb 100644 --- a/workspace_tests/src/cli/output/cli_output_builder.rs +++ b/workspace_tests/src/cli/output/cli_output_builder.rs @@ -105,8 +105,8 @@ async fn build_passes_through_progress_format() -> Result<(), Box Result<(), Box> { +async fn build_colorize_auto_passes_uncolored_for_non_interactive_terminal( +) -> Result<(), Box> { let cli_output = CliOutputBuilder::new().build(); assert_eq!(CliColorize::Uncolored, cli_output.colorize()); @@ -115,8 +115,8 @@ async fn build_colorize_auto_passes_uncolored_for_non_interactive_terminal() #[cfg(feature = "output_progress")] #[tokio::test] -async fn build_progress_format_auto_passes_stderr_for_non_interactive_terminal() --> Result<(), Box> { +async fn build_progress_format_auto_passes_stderr_for_non_interactive_terminal( +) -> Result<(), Box> { let cli_output = CliOutputBuilder::new().build(); assert_eq!(CliProgressFormat::Outcome, cli_output.progress_format()); diff --git a/workspace_tests/src/cmd/ctx/cmd_ctx.rs b/workspace_tests/src/cmd/ctx/cmd_ctx.rs index 0a4bc4653..041885d36 100644 --- a/workspace_tests/src/cmd/ctx/cmd_ctx.rs +++ b/workspace_tests/src/cmd/ctx/cmd_ctx.rs @@ -17,7 +17,7 @@ async fn single_profile_single_flow_getters() -> Result<(), Box Result<(), Box Result<(), Box> { +async fn build_with_workspace_params_with_profile_params_with_profile_filter( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let profile = profile!("test_profile"); let profile_other = profile!("test_profile_other"); diff --git a/workspace_tests/src/cmd/ctx/cmd_ctx_builder/multi_profile_single_flow_builder.rs b/workspace_tests/src/cmd/ctx/cmd_ctx_builder/multi_profile_single_flow_builder.rs index e5a8b9c7c..f82f447fa 100644 --- a/workspace_tests/src/cmd/ctx/cmd_ctx_builder/multi_profile_single_flow_builder.rs +++ b/workspace_tests/src/cmd/ctx/cmd_ctx_builder/multi_profile_single_flow_builder.rs @@ -26,7 +26,7 @@ async fn build() -> Result<(), Box> { let output = NoOpOutput; let cmd_ctx = CmdCtx::builder_multi_profile_single_flow(output.into(), (&workspace).into()) - .with_flow(&flow) + .with_flow((&flow).into()) .build() .await?; @@ -84,7 +84,7 @@ async fn build_with_workspace_params() -> Result<(), Box> let output = NoOpOutput; let cmd_ctx = CmdCtx::builder_multi_profile_single_flow(output.into(), (&workspace).into()) - .with_flow(&flow) + .with_flow((&flow).into()) .with_workspace_param_value(String::from("profile"), Some(profile.clone())) .with_workspace_param_value( String::from("ws_param_1"), @@ -156,7 +156,7 @@ async fn build_with_profile_params() -> Result<(), Box> { .with_profile_params_k::() .with_profile_param::(String::from("profile_param_0")) .with_profile_param::(String::from("profile_param_1")) - .with_flow(&flow) + .with_flow((&flow).into()) .build() .await?; @@ -214,7 +214,7 @@ async fn build_with_flow_params() -> Result<(), Box> { let output = NoOpOutput; let cmd_ctx = CmdCtx::builder_multi_profile_single_flow(output.into(), (&workspace).into()) - .with_flow(&flow) + .with_flow((&flow).into()) .with_flow_params_k::() .with_flow_param::(String::from("flow_param_0")) .with_flow_param::(String::from("flow_param_1")) @@ -282,7 +282,7 @@ async fn build_with_workspace_params_with_profile_params() -> Result<(), Box() .with_profile_param::(String::from("profile_param_0")) .with_workspace_param_value(String::from("profile"), Some(profile.clone())) @@ -344,8 +344,8 @@ async fn build_with_workspace_params_with_profile_params() -> Result<(), Box Result<(), Box> { +async fn build_with_workspace_params_with_profile_params_with_flow_params( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let profile = profile!("test_profile"); let profile_other = profile!("test_profile_other"); @@ -361,7 +361,7 @@ async fn build_with_workspace_params_with_profile_params_with_flow_params() let output = NoOpOutput; let cmd_ctx = CmdCtx::builder_multi_profile_single_flow(output.into(), (&workspace).into()) - .with_flow(&flow) + .with_flow((&flow).into()) .with_profile_params_k::() .with_profile_param::(String::from("profile_param_0")) .with_flow_params_k::() @@ -455,7 +455,7 @@ async fn build_with_workspace_params_with_profile_filter() -> Result<(), Box Result<(), Box Result<(), Box> { +async fn build_with_workspace_params_with_profile_params_with_profile_filter( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let profile = profile!("test_profile"); let profile_other = profile!("test_profile_other"); @@ -525,7 +525,7 @@ async fn build_with_workspace_params_with_profile_params_with_profile_filter() .with_flow_param::(String::from("flow_param_0")) .with_profile_filter(|profile| **profile == "test_profile") .with_flow_param::(String::from("flow_param_1")) - .with_flow(&flow) + .with_flow((&flow).into()) .build() .await?; @@ -597,7 +597,7 @@ async fn getters() -> Result<(), Box> { output.into(), (&workspace).into(), ) - .with_flow(&flow) + .with_flow((&flow).into()) .build() .await?; @@ -640,7 +640,7 @@ async fn debug() -> Result<(), Box> { output.into(), (&workspace).into(), ) - .with_flow(&flow) + .with_flow((&flow).into()) .build() .await?; diff --git a/workspace_tests/src/cmd/ctx/cmd_ctx_builder/single_profile_no_flow_builder.rs b/workspace_tests/src/cmd/ctx/cmd_ctx_builder/single_profile_no_flow_builder.rs index 67890f55a..fe4e81090 100644 --- a/workspace_tests/src/cmd/ctx/cmd_ctx_builder/single_profile_no_flow_builder.rs +++ b/workspace_tests/src/cmd/ctx/cmd_ctx_builder/single_profile_no_flow_builder.rs @@ -152,8 +152,8 @@ async fn build_with_workspace_params_with_profile_params() -> Result<(), Box Result<(), Box> { +async fn build_with_workspace_params_with_profile_from_params( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = workspace(&tempdir, app_name!("test_single_profile_no_flow"))?; let profile = profile!("test_profile"); @@ -168,7 +168,7 @@ async fn build_with_workspace_params_with_profile_from_params() String::from("ws_param_1"), Some("ws_param_1_value".to_string()), ) - .with_profile_from_workspace_param(&String::from("profile")) + .with_profile_from_workspace_param(String::from("profile").into()) .build() .await?; @@ -192,8 +192,8 @@ async fn build_with_workspace_params_with_profile_from_params() } #[tokio::test] -async fn build_with_workspace_params_with_profile_params_with_profile_from_params() --> Result<(), Box> { +async fn build_with_workspace_params_with_profile_params_with_profile_from_params( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = workspace(&tempdir, app_name!("test_single_profile_no_flow"))?; let profile = profile!("test_profile"); @@ -210,7 +210,7 @@ async fn build_with_workspace_params_with_profile_params_with_profile_from_param String::from("ws_param_1"), Some("ws_param_1_value".to_string()), ) - .with_profile_from_workspace_param(&String::from("profile")) + .with_profile_from_workspace_param(String::from("profile").into()) .build() .await?; diff --git a/workspace_tests/src/cmd/ctx/cmd_ctx_builder/single_profile_single_flow_builder.rs b/workspace_tests/src/cmd/ctx/cmd_ctx_builder/single_profile_single_flow_builder.rs index 32fba8ca6..52f0ac4c5 100644 --- a/workspace_tests/src/cmd/ctx/cmd_ctx_builder/single_profile_single_flow_builder.rs +++ b/workspace_tests/src/cmd/ctx/cmd_ctx_builder/single_profile_single_flow_builder.rs @@ -30,7 +30,7 @@ async fn build() -> Result<(), Box> { (&workspace).into(), ) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .build() .await?; @@ -64,7 +64,7 @@ async fn build_with_workspace_params() -> Result<(), Box> (&workspace).into(), ) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .with_workspace_param_value(String::from("profile"), Some(profile.clone())) .with_workspace_param_value( String::from("ws_param_1"), @@ -116,7 +116,7 @@ async fn build_with_profile_params() -> Result<(), Box> { .with_profile_param_value(String::from("profile_param_0"), Some(1u32)) .with_profile_param_value(String::from("profile_param_1"), Some(2u64)) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .build() .await?; @@ -153,7 +153,7 @@ async fn build_with_flow_params() -> Result<(), Box> { (&workspace).into(), ) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .with_flow_param_value(String::from("flow_param_0"), Some(true)) .with_flow_param_value(String::from("flow_param_1"), Some(456u16)) .build() @@ -196,7 +196,7 @@ async fn build_with_workspace_params_with_profile_params() -> Result<(), Box Result<(), Box Result<(), Box> { +async fn build_with_workspace_params_with_profile_params_with_flow_params( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = workspace(&tempdir, app_name!("test_single_profile_single_flow"))?; let profile = profile!("test_profile"); @@ -253,7 +253,7 @@ async fn build_with_workspace_params_with_profile_params_with_flow_params() (&workspace).into(), ) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .with_profile_param_value(String::from("profile_param_0"), Some(1u32)) .with_flow_param_value(String::from("flow_param_0"), Some(true)) .with_workspace_param_value(String::from("profile"), Some(profile.clone())) @@ -302,8 +302,8 @@ async fn build_with_workspace_params_with_profile_params_with_flow_params() } #[tokio::test] -async fn build_with_workspace_params_with_profile_from_params() --> Result<(), Box> { +async fn build_with_workspace_params_with_profile_from_params( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = workspace(&tempdir, app_name!("test_single_profile_single_flow"))?; let profile = profile!("test_profile"); @@ -320,8 +320,8 @@ async fn build_with_workspace_params_with_profile_from_params() String::from("ws_param_1"), Some("ws_param_1_value".to_string()), ) - .with_profile_from_workspace_param(&String::from("profile")) - .with_flow(&flow) + .with_profile_from_workspace_param(String::from("profile").into()) + .with_flow((&flow).into()) .build() .await?; @@ -353,8 +353,8 @@ async fn build_with_workspace_params_with_profile_from_params() } #[tokio::test] -async fn build_with_workspace_params_with_profile_params_with_profile_from_params() --> Result<(), Box> { +async fn build_with_workspace_params_with_profile_params_with_profile_from_params( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = workspace(&tempdir, app_name!("test_single_profile_single_flow"))?; let profile = profile!("test_profile"); @@ -374,9 +374,9 @@ async fn build_with_workspace_params_with_profile_params_with_profile_from_param Some("ws_param_1_value".to_string()), ) .with_flow_param_value(String::from("flow_param_0"), Some(true)) - .with_profile_from_workspace_param(&String::from("profile")) + .with_profile_from_workspace_param(String::from("profile").into()) .with_flow_param_value(String::from("flow_param_1"), Some(456u16)) - .with_flow(&flow) + .with_flow((&flow).into()) .build() .await?; @@ -416,8 +416,8 @@ async fn build_with_workspace_params_with_profile_params_with_profile_from_param } #[tokio::test] -async fn build_with_item_params_returns_ok_when_params_provided() --> Result<(), Box> { +async fn build_with_item_params_returns_ok_when_params_provided( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = workspace(&tempdir, app_name!("test_single_profile_single_flow"))?; let profile = profile!("test_profile"); @@ -435,7 +435,7 @@ async fn build_with_item_params_returns_ok_when_params_provided() (&workspace).into(), ) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::(VecCopyItem::ID_DEFAULT.clone(), VecA(vec![1u8]).into()) .build() .await?; @@ -465,8 +465,8 @@ async fn build_with_item_params_returns_ok_when_params_provided() } #[tokio::test] -async fn build_with_item_params_returns_err_when_params_not_provided_and_not_stored() --> Result<(), Box> { +async fn build_with_item_params_returns_err_when_params_not_provided_and_not_stored( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = workspace(&tempdir, app_name!("test_single_profile_single_flow"))?; let profile = profile!("test_profile"); @@ -484,7 +484,7 @@ async fn build_with_item_params_returns_err_when_params_not_provided_and_not_sto (&workspace).into(), ) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .build() .await; @@ -516,8 +516,8 @@ async fn build_with_item_params_returns_err_when_params_not_provided_and_not_sto } #[tokio::test] -async fn build_with_item_params_returns_ok_when_params_not_provided_but_are_stored() --> Result<(), Box> { +async fn build_with_item_params_returns_ok_when_params_not_provided_but_are_stored( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = workspace(&tempdir, app_name!("test_single_profile_single_flow"))?; let profile = profile!("test_profile"); @@ -535,7 +535,7 @@ async fn build_with_item_params_returns_ok_when_params_not_provided_but_are_stor (&workspace).into(), ) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::(VecCopyItem::ID_DEFAULT.clone(), VecA(vec![1u8]).into()) .build() .await?; @@ -545,7 +545,7 @@ async fn build_with_item_params_returns_ok_when_params_not_provided_but_are_stor NoOpOutput, >((&mut output).into(), (&workspace).into()) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .build() .await?; @@ -574,8 +574,8 @@ async fn build_with_item_params_returns_ok_when_params_not_provided_but_are_stor } #[tokio::test] -async fn build_with_item_params_returns_ok_and_uses_params_provided_when_params_provided_and_stored() --> Result<(), Box> { +async fn build_with_item_params_returns_ok_and_uses_params_provided_when_params_provided_and_stored( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = workspace(&tempdir, app_name!("test_single_profile_single_flow"))?; let profile = profile!("test_profile"); @@ -593,7 +593,7 @@ async fn build_with_item_params_returns_ok_and_uses_params_provided_when_params_ (&workspace).into(), ) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::(VecCopyItem::ID_DEFAULT.clone(), VecA(vec![1u8]).into()) .build() .await?; @@ -603,7 +603,7 @@ async fn build_with_item_params_returns_ok_and_uses_params_provided_when_params_ NoOpOutput, >((&mut output).into(), (&workspace).into()) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::(VecCopyItem::ID_DEFAULT.clone(), VecA(vec![2u8]).into()) .build() .await?; @@ -633,8 +633,8 @@ async fn build_with_item_params_returns_ok_and_uses_params_provided_when_params_ } #[tokio::test] -async fn build_with_item_params_returns_err_when_params_provided_mismatch() --> Result<(), Box> { +async fn build_with_item_params_returns_err_when_params_provided_mismatch( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = workspace(&tempdir, app_name!("test_single_profile_single_flow"))?; let profile = profile!("test_profile"); @@ -652,7 +652,7 @@ async fn build_with_item_params_returns_err_when_params_provided_mismatch() (&workspace).into(), ) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::(VecCopyItem::ID_DEFAULT.clone(), VecA(vec![1u8]).into()) .build() .await?; @@ -662,7 +662,7 @@ async fn build_with_item_params_returns_err_when_params_provided_mismatch() (&workspace).into(), ) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::(item_id!("mismatch_id"), VecA(vec![2u8]).into()) .build() .await; @@ -688,7 +688,7 @@ async fn build_with_item_params_returns_err_when_params_provided_mismatch() if value == &vec![2u8] ) && matches!( - params_specs_stored_mismatches, + params_specs_stored_mismatches.as_ref(), Some(params_specs_stored_mismatches) if params_specs_stored_mismatches.is_empty() ) @@ -703,8 +703,8 @@ async fn build_with_item_params_returns_err_when_params_provided_mismatch() } #[tokio::test] -async fn build_with_item_params_returns_err_when_params_stored_mismatch() --> Result<(), Box> { +async fn build_with_item_params_returns_err_when_params_stored_mismatch( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = workspace(&tempdir, app_name!("test_single_profile_single_flow"))?; let profile = profile!("test_profile"); @@ -722,7 +722,7 @@ async fn build_with_item_params_returns_err_when_params_stored_mismatch() (&workspace).into(), ) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::(item_id!("original_id"), VecA(vec![1u8]).into()) .build() .await?; @@ -742,7 +742,7 @@ async fn build_with_item_params_returns_err_when_params_stored_mismatch() (&workspace).into(), ) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::(item_id!("mismatch_id"), VecA(vec![2u8]).into()) .build() .await; @@ -768,7 +768,7 @@ async fn build_with_item_params_returns_err_when_params_stored_mismatch() if value == &vec![2u8] ) && matches!( - params_specs_stored_mismatches, + params_specs_stored_mismatches.as_ref(), Some(params_specs_stored_mismatches) if params_specs_stored_mismatches.is_empty() ) @@ -783,8 +783,8 @@ async fn build_with_item_params_returns_err_when_params_stored_mismatch() } #[tokio::test] -async fn build_with_item_params_returns_ok_when_spec_provided_for_previous_mapping_fn() --> Result<(), Box> { +async fn build_with_item_params_returns_ok_when_spec_provided_for_previous_mapping_fn( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = workspace(&tempdir, app_name!("test_single_profile_single_flow"))?; let profile = profile!("test_profile"); @@ -802,7 +802,8 @@ async fn build_with_item_params_returns_ok_when_spec_provided_for_previous_mappi (&workspace).into(), ) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) + .with_resource(0u8) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA::field_wise_spec() @@ -823,7 +824,7 @@ async fn build_with_item_params_returns_ok_when_spec_provided_for_previous_mappi (&workspace).into(), ) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA::field_wise_spec() @@ -869,8 +870,8 @@ async fn build_with_item_params_returns_ok_when_spec_provided_for_previous_mappi } #[tokio::test] -async fn build_with_item_params_returns_err_when_spec_fully_not_provided_for_previous_mapping_fn() --> Result<(), Box> { +async fn build_with_item_params_returns_err_when_spec_fully_not_provided_for_previous_mapping_fn( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = workspace(&tempdir, app_name!("test_single_profile_single_flow"))?; let profile = profile!("test_profile"); @@ -888,7 +889,8 @@ async fn build_with_item_params_returns_err_when_spec_fully_not_provided_for_pre (&workspace).into(), ) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) + .with_resource(0u8) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA::field_wise_spec() @@ -909,7 +911,7 @@ async fn build_with_item_params_returns_err_when_spec_fully_not_provided_for_pre (&workspace).into(), ) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) // Note: no item_params for `VecCopyItem` .build() .await; @@ -931,7 +933,7 @@ async fn build_with_item_params_returns_err_when_spec_fully_not_provided_for_pre if item_ids_with_no_params_specs.is_empty() && params_specs_provided_mismatches.is_empty() && matches!( - params_specs_stored_mismatches, + params_specs_stored_mismatches.as_ref(), Some(params_specs_stored_mismatches) if params_specs_stored_mismatches.is_empty() ) @@ -946,8 +948,8 @@ async fn build_with_item_params_returns_err_when_spec_fully_not_provided_for_pre } #[tokio::test] -async fn build_with_item_params_returns_err_when_value_spec_not_provided_for_previous_mapping_fn() --> Result<(), Box> { +async fn build_with_item_params_returns_err_when_value_spec_not_provided_for_previous_mapping_fn( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = workspace(&tempdir, app_name!("test_single_profile_single_flow"))?; let profile = profile!("test_profile"); @@ -965,7 +967,8 @@ async fn build_with_item_params_returns_err_when_value_spec_not_provided_for_pre (&workspace).into(), ) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) + .with_resource(0u8) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA::field_wise_spec() @@ -986,7 +989,7 @@ async fn build_with_item_params_returns_err_when_value_spec_not_provided_for_pre (&workspace).into(), ) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) // Note: item_params provided, but not enough to replace mapping function. .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), @@ -1012,7 +1015,7 @@ async fn build_with_item_params_returns_err_when_value_spec_not_provided_for_pre if item_ids_with_no_params_specs.is_empty() && params_specs_provided_mismatches.is_empty() && matches!( - params_specs_stored_mismatches, + params_specs_stored_mismatches.as_ref(), Some(params_specs_stored_mismatches) if params_specs_stored_mismatches.is_empty() ) @@ -1027,8 +1030,8 @@ async fn build_with_item_params_returns_err_when_value_spec_not_provided_for_pre } #[tokio::test] -async fn build_with_item_params_returns_params_specs_mismatch_err_when_item_renamed() --> Result<(), Box> { +async fn build_with_item_params_returns_params_specs_mismatch_err_when_item_renamed( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = workspace(&tempdir, app_name!("test_single_profile_single_flow"))?; let profile = profile!("test_profile"); @@ -1046,7 +1049,7 @@ async fn build_with_item_params_returns_params_specs_mismatch_err_when_item_rena (&workspace).into(), ) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::(item_id!("original_id"), VecA(vec![1u8]).into()) .build() .await?; @@ -1063,7 +1066,7 @@ async fn build_with_item_params_returns_params_specs_mismatch_err_when_item_rena (&workspace).into(), ) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::(item_id!("mismatch_id"), VecA(vec![2u8]).into()) .build() .await; @@ -1078,7 +1081,7 @@ async fn build_with_item_params_returns_params_specs_mismatch_err_when_item_rena peace::rt_model::Error::ParamsSpecsMismatch { item_ids_with_no_params_specs, params_specs_provided_mismatches, - params_specs_stored_mismatches: Some(params_specs_stored_mismatches), + params_specs_stored_mismatches, params_specs_not_usable, } )) @@ -1095,7 +1098,11 @@ async fn build_with_item_params_returns_params_specs_mismatch_err_when_item_rena ) }) .unwrap_or(false) - && params_specs_stored_mismatches.is_empty() + && matches!( + params_specs_stored_mismatches.as_ref(), + Some(params_specs_stored_mismatches) + if params_specs_stored_mismatches.is_empty() + ) && params_specs_not_usable.is_empty() ), "was {cmd_ctx_result:#?}" @@ -1107,8 +1114,8 @@ async fn build_with_item_params_returns_params_specs_mismatch_err_when_item_rena } #[tokio::test] -async fn build_with_item_params_returns_ok_when_new_item_added_with_params_provided() --> Result<(), Box> { +async fn build_with_item_params_returns_ok_when_new_item_added_with_params_provided( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = workspace(&tempdir, app_name!("test_single_profile_single_flow"))?; let profile = profile!("test_profile"); @@ -1124,7 +1131,7 @@ async fn build_with_item_params_returns_ok_when_new_item_added_with_params_provi (&workspace).into(), ) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .build() .await?; @@ -1140,7 +1147,7 @@ async fn build_with_item_params_returns_ok_when_new_item_added_with_params_provi (&workspace).into(), ) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::(VecCopyItem::ID_DEFAULT.clone(), VecA(vec![1u8]).into()) .build() .await?; diff --git a/workspace_tests/src/cmd_rt/cmd_execution.rs b/workspace_tests/src/cmd_rt/cmd_execution.rs index ff9b479f5..4aba7d292 100644 --- a/workspace_tests/src/cmd_rt/cmd_execution.rs +++ b/workspace_tests/src/cmd_rt/cmd_execution.rs @@ -40,7 +40,7 @@ async fn runs_one_cmd_block() -> Result<(), PeaceTestError> { let output = NoOpOutput; let mut cmd_ctx = CmdCtx::builder_single_profile_single_flow(output.into(), workspace.into()) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -97,7 +97,7 @@ async fn chains_multiple_cmd_blocks() -> Result<(), PeaceTestError> { let output = NoOpOutput; let mut cmd_ctx = CmdCtx::builder_single_profile_single_flow(output.into(), workspace.into()) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), diff --git a/workspace_tests/src/cmd_rt/cmd_execution/cmd_execution_error_builder.rs b/workspace_tests/src/cmd_rt/cmd_execution/cmd_execution_error_builder.rs index f499d96ab..cc3de90a6 100644 --- a/workspace_tests/src/cmd_rt/cmd_execution/cmd_execution_error_builder.rs +++ b/workspace_tests/src/cmd_rt/cmd_execution/cmd_execution_error_builder.rs @@ -44,7 +44,7 @@ async fn builds_error_for_missing_input_tuple_first_parameter() -> Result<(), Pe let output = NoOpOutput; let mut cmd_ctx = CmdCtx::builder_single_profile_single_flow(output.into(), workspace.into()) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -133,7 +133,7 @@ async fn builds_error_for_missing_input_tuple_second_parameter() -> Result<(), P let output = NoOpOutput; let mut cmd_ctx = CmdCtx::builder_single_profile_single_flow(output.into(), workspace.into()) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), diff --git a/workspace_tests/src/flow_model/flow_spec_info.rs b/workspace_tests/src/flow_model/flow_spec_info.rs index 512f8ad52..e4ac9f0bb 100644 --- a/workspace_tests/src/flow_model/flow_spec_info.rs +++ b/workspace_tests/src/flow_model/flow_spec_info.rs @@ -7,7 +7,7 @@ use peace::{ model::{ common::{Edges, NodeHierarchy, NodeNames}, edge_id, - info_graph::{GraphDir, InfoGraph}, + info_graph::{GraphDir, GraphStyle, InfoGraph}, node_id, }, }, @@ -50,59 +50,12 @@ fn to_progress_info_graph() -> Result<(), Box> { node_names.insert(node_id!("e"), String::from("e")); node_names.insert(node_id!("f"), String::from("f")); - InfoGraph::builder() + InfoGraph::default() + .with_graph_style(GraphStyle::Circle) .with_direction(GraphDir::Vertical) .with_hierarchy(node_hierarchy) .with_node_names(node_names) .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 = Edges::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_names = NodeNames::new(); - node_names.insert(node_id!("a"), String::from("a")); - node_names.insert(node_id!("b"), String::from("b")); - node_names.insert(node_id!("c"), String::from("c")); - node_names.insert(node_id!("d"), String::from("d")); - node_names.insert(node_id!("e"), String::from("e")); - node_names.insert(node_id!("f"), String::from("f")); - - InfoGraph::builder() - .with_direction(GraphDir::Vertical) - .with_hierarchy(node_hierarchy) - .with_node_names(node_names) - .with_edges(edges) - .build() }; assert_eq!(info_graph_expected, info_graph); diff --git a/workspace_tests/src/fn_tracker_output.rs b/workspace_tests/src/fn_tracker_output.rs index 0ac3d4456..2d2501310 100644 --- a/workspace_tests/src/fn_tracker_output.rs +++ b/workspace_tests/src/fn_tracker_output.rs @@ -9,7 +9,11 @@ use crate::FnInvocation; cfg_if::cfg_if! { if #[cfg(feature = "output_progress")] { use peace::{ - cfg::progress::{ProgressTracker, ProgressUpdateAndId}, + cfg::{ + progress::{CmdBlockItemInteractionType, ProgressTracker, ProgressUpdateAndId}, + ItemId, + }, + item_model::ItemLocationState, rt_model::CmdProgressTracker, }; } @@ -42,6 +46,21 @@ where #[cfg(feature = "output_progress")] async fn progress_begin(&mut self, _cmd_progress_tracker: &CmdProgressTracker) {} + #[cfg(feature = "output_progress")] + async fn cmd_block_start( + &mut self, + _cmd_block_item_interaction_type: CmdBlockItemInteractionType, + ) { + } + + #[cfg(feature = "output_progress")] + async fn item_location_state( + &mut self, + _item_id: ItemId, + _item_location_state: ItemLocationState, + ) { + } + #[cfg(feature = "output_progress")] async fn progress_update( &mut self, diff --git a/workspace_tests/src/item_model.rs b/workspace_tests/src/item_model.rs new file mode 100644 index 000000000..f43cdc364 --- /dev/null +++ b/workspace_tests/src/item_model.rs @@ -0,0 +1,2 @@ +mod item_interaction; +mod item_location; diff --git a/workspace_tests/src/item_model/item_interaction.rs b/workspace_tests/src/item_model/item_interaction.rs new file mode 100644 index 000000000..83c9d4374 --- /dev/null +++ b/workspace_tests/src/item_model/item_interaction.rs @@ -0,0 +1,47 @@ +use peace::item_model::{ + ItemInteraction, ItemInteractionPull, ItemInteractionPush, ItemInteractionWithin, ItemLocation, +}; + +mod item_interaction_pull; +mod item_interaction_push; +mod item_interaction_within; + +#[test] +fn from_item_interaction_push() { + let item_interaction_push = ItemInteractionPush::new( + vec![ItemLocation::localhost()].into(), + vec![ItemLocation::host("server".to_string())].into(), + ); + let item_interaction = ItemInteraction::from(item_interaction_push.clone()); + + assert_eq!( + ItemInteraction::Push(item_interaction_push), + item_interaction + ); +} + +#[test] +fn from_item_interaction_pull() { + let item_interaction_pull = ItemInteractionPull::new( + vec![ItemLocation::localhost()].into(), + vec![ItemLocation::host("server".to_string())].into(), + ); + let item_interaction = ItemInteraction::from(item_interaction_pull.clone()); + + assert_eq!( + ItemInteraction::Pull(item_interaction_pull), + item_interaction + ); +} + +#[test] +fn from_item_interaction_within() { + let item_interaction_within = + ItemInteractionWithin::new(vec![ItemLocation::localhost()].into()); + let item_interaction = ItemInteraction::from(item_interaction_within.clone()); + + assert_eq!( + ItemInteraction::Within(item_interaction_within), + item_interaction + ); +} diff --git a/workspace_tests/src/item_model/item_interaction/item_interaction_pull.rs b/workspace_tests/src/item_model/item_interaction/item_interaction_pull.rs new file mode 100644 index 000000000..46da72d5b --- /dev/null +++ b/workspace_tests/src/item_model/item_interaction/item_interaction_pull.rs @@ -0,0 +1,27 @@ +use peace::item_model::{ItemInteractionPull, ItemLocation}; + +#[test] +fn location_client() { + let item_interaction_pull = ItemInteractionPull::new( + vec![ItemLocation::localhost()].into(), + vec![ItemLocation::host("server".to_string())].into(), + ); + + assert_eq!( + vec![ItemLocation::localhost()], + item_interaction_pull.location_client() + ); +} + +#[test] +fn location_server() { + let item_interaction_pull = ItemInteractionPull::new( + vec![ItemLocation::localhost()].into(), + vec![ItemLocation::host("server".to_string())].into(), + ); + + assert_eq!( + vec![ItemLocation::host("server".to_string())], + item_interaction_pull.location_server() + ); +} diff --git a/workspace_tests/src/item_model/item_interaction/item_interaction_push.rs b/workspace_tests/src/item_model/item_interaction/item_interaction_push.rs new file mode 100644 index 000000000..7e2711f9a --- /dev/null +++ b/workspace_tests/src/item_model/item_interaction/item_interaction_push.rs @@ -0,0 +1,27 @@ +use peace::item_model::{ItemInteractionPush, ItemLocation}; + +#[test] +fn location_from() { + let item_interaction_push = ItemInteractionPush::new( + vec![ItemLocation::localhost()].into(), + vec![ItemLocation::host("server".to_string())].into(), + ); + + assert_eq!( + vec![ItemLocation::localhost()], + item_interaction_push.location_from() + ); +} + +#[test] +fn location_to() { + let item_interaction_push = ItemInteractionPush::new( + vec![ItemLocation::localhost()].into(), + vec![ItemLocation::host("server".to_string())].into(), + ); + + assert_eq!( + vec![ItemLocation::host("server".to_string())], + item_interaction_push.location_to() + ); +} diff --git a/workspace_tests/src/item_model/item_interaction/item_interaction_within.rs b/workspace_tests/src/item_model/item_interaction/item_interaction_within.rs new file mode 100644 index 000000000..f4637ed30 --- /dev/null +++ b/workspace_tests/src/item_model/item_interaction/item_interaction_within.rs @@ -0,0 +1,12 @@ +use peace::item_model::{ItemInteractionWithin, ItemLocation}; + +#[test] +fn location() { + let item_interaction_within = + ItemInteractionWithin::new(vec![ItemLocation::localhost()].into()); + + assert_eq!( + vec![ItemLocation::localhost()], + item_interaction_within.location() + ); +} diff --git a/workspace_tests/src/item_model/item_location.rs b/workspace_tests/src/item_model/item_location.rs new file mode 100644 index 000000000..bdac4066a --- /dev/null +++ b/workspace_tests/src/item_model/item_location.rs @@ -0,0 +1,130 @@ +use std::{ffi::OsStr, path::Path}; + +use peace::item_model::{url::ParseError, ItemLocation, ItemLocationType, Url}; + +#[test] +fn group() { + let item_location = ItemLocation::group("Cloud".to_string()); + + assert_eq!( + ItemLocation::new( + peace::item_model::ItemLocationType::Group, + "Cloud".to_string(), + ), + item_location + ); +} + +#[test] +fn host() { + let item_location = ItemLocation::host("Server".to_string()); + + assert_eq!( + ItemLocation::new( + peace::item_model::ItemLocationType::Host, + "Server".to_string(), + ), + item_location + ); +} + +#[test] +fn host_unknown() { + let item_location = ItemLocation::host_unknown(); + + assert_eq!( + ItemLocation::new( + peace::item_model::ItemLocationType::Host, + ItemLocation::HOST_UNKNOWN.to_string(), + ), + item_location + ); +} + +#[test] +fn host_from_url_https() -> Result<(), ParseError> { + let item_location = ItemLocation::host_from_url(&Url::parse("https://example.com/resource")?); + + assert_eq!( + ItemLocation::new( + peace::item_model::ItemLocationType::Host, + "🌐 example.com".to_string(), + ), + item_location + ); + + Ok(()) +} + +#[test] +fn host_from_url_file() -> Result<(), ParseError> { + let item_location = ItemLocation::host_from_url(&Url::parse("file:///path/to/resource")?); + + assert_eq!( + ItemLocation::new( + peace::item_model::ItemLocationType::Host, + ItemLocation::LOCALHOST.to_string(), + ), + item_location + ); + + Ok(()) +} + +#[test] +fn localhost() { + let item_location = ItemLocation::localhost(); + + assert_eq!( + ItemLocation::new( + peace::item_model::ItemLocationType::Host, + ItemLocation::LOCALHOST.to_string(), + ), + item_location + ); +} + +#[test] +fn path() { + let item_location = ItemLocation::path("/path/to/resource".to_string()); + + assert_eq!( + ItemLocation::new( + peace::item_model::ItemLocationType::Path, + "/path/to/resource".to_string(), + ), + item_location + ); +} + +#[test] +fn path_lossy() { + let path = unsafe { + Path::new(OsStr::from_encoded_bytes_unchecked( + b"/path/to/lossy_fo\xF0\x90\x80.txt", + )) + }; + let item_location = ItemLocation::path_lossy(path); + + assert_eq!( + ItemLocation::new( + peace::item_model::ItemLocationType::Path, + "/path/to/lossy_fo�.txt".to_string(), + ), + item_location + ); +} + +#[test] +fn name() { + let item_location = ItemLocation::path("/path/to/resource".to_string()); + + assert_eq!("/path/to/resource", item_location.name()); +} + +#[test] +fn r#type() { + let item_location = ItemLocation::path("/path/to/resource".to_string()); + + assert_eq!(ItemLocationType::Path, item_location.r#type()); +} diff --git a/workspace_tests/src/items/sh_cmd_item.rs b/workspace_tests/src/items/sh_cmd_item.rs index bfede52ea..b220f2ab4 100644 --- a/workspace_tests/src/items/sh_cmd_item.rs +++ b/workspace_tests/src/items/sh_cmd_item.rs @@ -1,5 +1,5 @@ use peace::{ - cfg::{app_name, item_id, profile, FlowId, ItemId, State}, + cfg::{app_name, item_id, profile, FlowId, ItemId}, cmd::ctx::CmdCtx, cmd_model::CmdOutcome, data::marker::Clean, @@ -7,16 +7,14 @@ use peace::{ rt_model::{Flow, InMemoryTextOutput, ItemGraphBuilder, Workspace, WorkspaceSpec}, }; use peace_items::sh_cmd::{ - ShCmd, ShCmdError, ShCmdExecutionRecord, ShCmdItem, ShCmdParams, ShCmdState, ShCmdStateDiff, + ShCmd, ShCmdError, ShCmdItem, ShCmdParams, ShCmdState, ShCmdStateDiff, ShCmdStateLogical, }; /// Creates a file. #[derive(Clone, Copy, Debug)] pub struct TestFileCreationShCmdItem; -pub type TestFileCreationShCmdStateLogical = ShCmdState; -pub type TestFileCreationShCmdState = - State; +pub type TestFileCreationShCmdState = ShCmdState; impl TestFileCreationShCmdItem { /// ID @@ -30,6 +28,10 @@ impl TestFileCreationShCmdItem { fn params() -> ShCmdParams { #[cfg(unix)] let sh_cmd_params = { + #[cfg(feature = "item_state_example")] + let state_example_sh_cmd = ShCmd::new("bash").arg("-c").arg(include_str!( + "sh_cmd_item/unix/test_file_creation_state_example.sh" + )); let state_clean_sh_cmd = ShCmd::new("bash").arg("-c").arg(include_str!( "sh_cmd_item/unix/test_file_creation_state_clean.sh" )); @@ -49,6 +51,8 @@ impl TestFileCreationShCmdItem { "sh_cmd_item/unix/test_file_creation_apply_exec.sh" )); ShCmdParams::::new( + #[cfg(feature = "item_state_example")] + state_example_sh_cmd, state_clean_sh_cmd, state_current_sh_cmd, state_goal_sh_cmd, @@ -60,6 +64,13 @@ impl TestFileCreationShCmdItem { #[cfg(windows)] let sh_cmd_params = { + #[cfg(feature = "item_state_example")] + let state_example_sh_cmd = + ShCmd::new("Powershell.exe") + .arg("-Command") + .arg(include_str!( + "sh_cmd_item/windows/test_file_creation_state_example.ps1" + )); let state_clean_sh_cmd = ShCmd::new("Powershell.exe") .arg("-Command") @@ -93,6 +104,8 @@ impl TestFileCreationShCmdItem { " }" )); ShCmdParams::::new( + #[cfg(feature = "item_state_example")] + state_example_sh_cmd, state_clean_sh_cmd, state_current_sh_cmd, state_goal_sh_cmd, @@ -140,7 +153,7 @@ async fn state_clean_returns_shell_command_clean_state() -> Result<(), Box>( TestFileCreationShCmdItem::ID, TestFileCreationShCmdItem::params().into(), @@ -157,11 +170,11 @@ async fn state_clean_returns_shell_command_clean_state() -> Result<(), Box` to be Some after `CleanCmd::exec_dry`." ); }; - if let ShCmdState::Some { + if let ShCmdStateLogical::Some { stdout, stderr, marker: _, - } = &state_clean.logical + } = &state_clean.0.logical { assert_eq!("not_exists", stdout); assert_eq!("`test_file` does not exist", stderr); @@ -173,8 +186,8 @@ async fn state_clean_returns_shell_command_clean_state() -> Result<(), Box Result<(), Box> { +async fn state_current_returns_shell_command_current_state( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -189,7 +202,7 @@ async fn state_current_returns_shell_command_current_state() let output = InMemoryTextOutput::new(); let mut cmd_ctx = CmdCtx::builder_single_profile_single_flow(output.into(), workspace.into()) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::>( TestFileCreationShCmdItem::ID, TestFileCreationShCmdItem::params().into(), @@ -206,11 +219,11 @@ async fn state_current_returns_shell_command_current_state() let state_current = states_current .get::(&TestFileCreationShCmdItem::ID) .unwrap(); - if let ShCmdState::Some { + if let ShCmdStateLogical::Some { stdout, stderr, marker: _, - } = &state_current.logical + } = &state_current.0.logical { assert_eq!("not_exists", stdout); assert_eq!("`test_file` does not exist", stderr); @@ -239,7 +252,7 @@ async fn state_goal_returns_shell_command_goal_state() -> Result<(), Box>( TestFileCreationShCmdItem::ID, TestFileCreationShCmdItem::params().into(), @@ -254,15 +267,13 @@ async fn state_goal_returns_shell_command_goal_state() -> Result<(), Box, _>( - &TestFileCreationShCmdItem::ID, - ) + .get::, _>(&TestFileCreationShCmdItem::ID) .unwrap(); - if let ShCmdState::Some { + if let ShCmdStateLogical::Some { stdout, stderr, marker: _, - } = &state_goal.logical + } = &state_goal.0.logical { assert_eq!("exists", stdout); assert_eq!("`test_file` exists", stderr); @@ -289,7 +300,7 @@ async fn state_diff_returns_shell_command_state_diff() -> Result<(), Box>( TestFileCreationShCmdItem::ID, TestFileCreationShCmdItem::params().into(), @@ -318,8 +329,8 @@ async fn state_diff_returns_shell_command_state_diff() -> Result<(), Box Result<(), Box> { +async fn ensure_when_creation_required_executes_apply_exec_shell_command( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -334,7 +345,7 @@ async fn ensure_when_creation_required_executes_apply_exec_shell_command() let output = InMemoryTextOutput::new(); let mut cmd_ctx = CmdCtx::builder_single_profile_single_flow(output.into(), workspace.into()) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::>( TestFileCreationShCmdItem::ID, TestFileCreationShCmdItem::params().into(), @@ -356,11 +367,11 @@ async fn ensure_when_creation_required_executes_apply_exec_shell_command() let state_ensured = states_ensured .get::(&TestFileCreationShCmdItem::ID) .unwrap(); - if let ShCmdState::Some { + if let ShCmdStateLogical::Some { stdout, stderr, marker: _, - } = &state_ensured.logical + } = &state_ensured.0.logical { assert_eq!("exists", stdout); assert_eq!("`test_file` exists", stderr); @@ -372,8 +383,8 @@ async fn ensure_when_creation_required_executes_apply_exec_shell_command() } #[tokio::test] -async fn ensure_when_exists_sync_does_not_reexecute_apply_exec_shell_command() --> Result<(), Box> { +async fn ensure_when_exists_sync_does_not_reexecute_apply_exec_shell_command( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -388,7 +399,7 @@ async fn ensure_when_exists_sync_does_not_reexecute_apply_exec_shell_command() let output = InMemoryTextOutput::new(); let mut cmd_ctx = CmdCtx::builder_single_profile_single_flow(output.into(), workspace.into()) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::>( TestFileCreationShCmdItem::ID, TestFileCreationShCmdItem::params().into(), @@ -428,11 +439,11 @@ async fn ensure_when_exists_sync_does_not_reexecute_apply_exec_shell_command() let state_ensured = states_ensured .get::(&TestFileCreationShCmdItem::ID) .unwrap(); - if let ShCmdState::Some { + if let ShCmdStateLogical::Some { stdout, stderr, marker: _, - } = &state_ensured.logical + } = &state_ensured.0.logical { assert_eq!("exists", stdout); assert_eq!("`test_file` exists", stderr); @@ -459,7 +470,7 @@ async fn clean_when_exists_sync_executes_shell_command() -> Result<(), Box>( TestFileCreationShCmdItem::ID, TestFileCreationShCmdItem::params().into(), @@ -491,11 +502,11 @@ async fn clean_when_exists_sync_executes_shell_command() -> Result<(), Box(&TestFileCreationShCmdItem::ID) .unwrap(); - if let ShCmdState::Some { + if let ShCmdStateLogical::Some { stdout, stderr, marker: _, - } = &state_cleaned.logical + } = &state_cleaned.0.logical { assert_eq!("not_exists", stdout); assert_eq!("`test_file` does not exist", stderr); diff --git a/workspace_tests/src/items/sh_cmd_item/unix/test_file_creation_state_example.sh b/workspace_tests/src/items/sh_cmd_item/unix/test_file_creation_state_example.sh new file mode 100644 index 000000000..ba3a122a9 --- /dev/null +++ b/workspace_tests/src/items/sh_cmd_item/unix/test_file_creation_state_example.sh @@ -0,0 +1,8 @@ +#! /bin/bash +set -euo pipefail + +# state +printf 'exists' + +# display string +printf '`test_file` exists' 1>&2 diff --git a/workspace_tests/src/items/sh_cmd_item/windows/test_file_creation_state_example.ps1 b/workspace_tests/src/items/sh_cmd_item/windows/test_file_creation_state_example.ps1 new file mode 100644 index 000000000..1336a59c4 --- /dev/null +++ b/workspace_tests/src/items/sh_cmd_item/windows/test_file_creation_state_example.ps1 @@ -0,0 +1,5 @@ +# state +Write-Host -NoNewLine 'exists' + +# display string +[Console]::Error.WriteLine('`test_file` exists') diff --git a/workspace_tests/src/items/tar_x_item.rs b/workspace_tests/src/items/tar_x_item.rs index c7649380e..31ad68a14 100644 --- a/workspace_tests/src/items/tar_x_item.rs +++ b/workspace_tests/src/items/tar_x_item.rs @@ -39,8 +39,8 @@ fn clone() { } #[tokio::test] -async fn state_current_returns_empty_file_metadatas_when_extraction_folder_not_exists() --> Result<(), Box> { +async fn state_current_returns_empty_file_metadatas_when_extraction_folder_not_exists( +) -> Result<(), Box> { let flow_id = FlowId::new(crate::fn_name_short!())?; let TestEnv { tempdir: _tempdir, @@ -55,7 +55,7 @@ async fn state_current_returns_empty_file_metadatas_when_extraction_folder_not_e let mut cmd_ctx = CmdCtx::builder_single_profile_single_flow(output.into(), workspace.into()) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::>( TarXTest::ID.clone(), TarXParams::::new(tar_path, dest).into(), @@ -79,8 +79,8 @@ async fn state_current_returns_empty_file_metadatas_when_extraction_folder_not_e } #[tokio::test] -async fn state_current_returns_file_metadatas_when_extraction_folder_contains_file() --> Result<(), Box> { +async fn state_current_returns_file_metadatas_when_extraction_folder_contains_file( +) -> Result<(), Box> { let flow_id = FlowId::new(crate::fn_name_short!())?; let TestEnv { tempdir: _tempdir, @@ -101,7 +101,7 @@ async fn state_current_returns_file_metadatas_when_extraction_folder_contains_fi let mut cmd_ctx = CmdCtx::builder_single_profile_single_flow(output.into(), workspace.into()) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::>( TarXTest::ID.clone(), TarXParams::::new(tar_path, dest).into(), @@ -148,7 +148,7 @@ async fn state_goal_returns_file_metadatas_from_tar() -> Result<(), Box>( TarXTest::ID.clone(), TarXParams::::new(tar_path, dest).into(), @@ -176,8 +176,8 @@ async fn state_goal_returns_file_metadatas_from_tar() -> Result<(), Box Result<(), Box> { +async fn state_diff_includes_added_when_file_in_tar_is_not_in_dest( +) -> Result<(), Box> { let flow_id = FlowId::new(crate::fn_name_short!())?; let TestEnv { tempdir: _tempdir, @@ -194,7 +194,7 @@ async fn state_diff_includes_added_when_file_in_tar_is_not_in_dest() let mut cmd_ctx = CmdCtx::builder_single_profile_single_flow(output.into(), workspace.into()) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::>( TarXTest::ID.clone(), TarXParams::::new(tar_path, dest).into(), @@ -228,8 +228,8 @@ async fn state_diff_includes_added_when_file_in_tar_is_not_in_dest() } #[tokio::test] -async fn state_diff_includes_added_when_file_in_tar_is_not_in_dest_and_dest_file_name_greater() --> Result<(), Box> { +async fn state_diff_includes_added_when_file_in_tar_is_not_in_dest_and_dest_file_name_greater( +) -> Result<(), Box> { let flow_id = FlowId::new(crate::fn_name_short!())?; let TestEnv { tempdir: _tempdir, @@ -252,7 +252,7 @@ async fn state_diff_includes_added_when_file_in_tar_is_not_in_dest_and_dest_file let mut cmd_ctx = CmdCtx::builder_single_profile_single_flow(output.into(), workspace.into()) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::>( TarXTest::ID.clone(), TarXParams::::new(tar_path, dest).into(), @@ -289,8 +289,8 @@ async fn state_diff_includes_added_when_file_in_tar_is_not_in_dest_and_dest_file } #[tokio::test] -async fn state_diff_includes_removed_when_file_in_dest_is_not_in_tar_and_tar_file_name_greater() --> Result<(), Box> { +async fn state_diff_includes_removed_when_file_in_dest_is_not_in_tar_and_tar_file_name_greater( +) -> Result<(), Box> { let flow_id = FlowId::new(crate::fn_name_short!())?; let TestEnv { tempdir: _tempdir, @@ -312,7 +312,7 @@ async fn state_diff_includes_removed_when_file_in_dest_is_not_in_tar_and_tar_fil let mut cmd_ctx = CmdCtx::builder_single_profile_single_flow(output.into(), workspace.into()) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::>( TarXTest::ID.clone(), TarXParams::::new(tar_path, dest).into(), @@ -347,8 +347,8 @@ async fn state_diff_includes_removed_when_file_in_dest_is_not_in_tar_and_tar_fil } #[tokio::test] -async fn state_diff_includes_removed_when_file_in_dest_is_not_in_tar_and_tar_file_name_lesser() --> Result<(), Box> { +async fn state_diff_includes_removed_when_file_in_dest_is_not_in_tar_and_tar_file_name_lesser( +) -> Result<(), Box> { let flow_id = FlowId::new(crate::fn_name_short!())?; let TestEnv { tempdir: _tempdir, @@ -370,7 +370,7 @@ async fn state_diff_includes_removed_when_file_in_dest_is_not_in_tar_and_tar_fil let mut cmd_ctx = CmdCtx::builder_single_profile_single_flow(output.into(), workspace.into()) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::>( TarXTest::ID.clone(), TarXParams::::new(tar_path, dest).into(), @@ -406,8 +406,8 @@ async fn state_diff_includes_removed_when_file_in_dest_is_not_in_tar_and_tar_fil } #[tokio::test] -async fn state_diff_includes_modified_when_dest_mtime_is_different() --> Result<(), Box> { +async fn state_diff_includes_modified_when_dest_mtime_is_different( +) -> Result<(), Box> { let flow_id = FlowId::new(crate::fn_name_short!())?; let TestEnv { tempdir: _tempdir, @@ -434,7 +434,7 @@ async fn state_diff_includes_modified_when_dest_mtime_is_different() let mut cmd_ctx = CmdCtx::builder_single_profile_single_flow(output.into(), workspace.into()) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::>( TarXTest::ID.clone(), TarXParams::::new(tar_path, dest).into(), @@ -472,8 +472,8 @@ async fn state_diff_includes_modified_when_dest_mtime_is_different() } #[tokio::test] -async fn state_diff_returns_extraction_in_sync_when_tar_and_dest_in_sync() --> Result<(), Box> { +async fn state_diff_returns_extraction_in_sync_when_tar_and_dest_in_sync( +) -> Result<(), Box> { let flow_id = FlowId::new(crate::fn_name_short!())?; let TestEnv { tempdir: _tempdir, @@ -492,7 +492,7 @@ async fn state_diff_returns_extraction_in_sync_when_tar_and_dest_in_sync() let mut cmd_ctx = CmdCtx::builder_single_profile_single_flow(output.into(), workspace.into()) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::>( TarXTest::ID.clone(), TarXParams::::new(tar_path, dest).into(), @@ -517,8 +517,8 @@ async fn state_diff_returns_extraction_in_sync_when_tar_and_dest_in_sync() } #[tokio::test] -async fn ensure_check_returns_exec_not_required_when_tar_and_dest_in_sync() --> Result<(), Box> { +async fn ensure_check_returns_exec_not_required_when_tar_and_dest_in_sync( +) -> Result<(), Box> { let flow_id = FlowId::new(crate::fn_name_short!())?; let TestEnv { tempdir: _tempdir, @@ -537,7 +537,7 @@ async fn ensure_check_returns_exec_not_required_when_tar_and_dest_in_sync() let mut cmd_ctx = CmdCtx::builder_single_profile_single_flow(output.into(), workspace.into()) .with_profile(profile.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::>( TarXTest::ID.clone(), TarXParams::::new(tar_path, dest).into(), @@ -611,7 +611,7 @@ async fn ensure_unpacks_tar_when_files_not_exists() -> Result<(), Box>( TarXTest::ID.clone(), TarXParams::::new(tar_path, dest).into(), @@ -670,7 +670,7 @@ async fn ensure_removes_other_files_and_is_idempotent() -> Result<(), Box>( TarXTest::ID.clone(), TarXParams::::new(tar_path, dest).into(), @@ -739,7 +739,7 @@ async fn clean_removes_files_in_dest_directory() -> Result<(), Box>( TarXTest::ID.clone(), TarXParams::::new(tar_path, dest.clone()).into(), diff --git a/workspace_tests/src/lib.rs b/workspace_tests/src/lib.rs index 8d41c5e9c..473330238 100644 --- a/workspace_tests/src/lib.rs +++ b/workspace_tests/src/lib.rs @@ -25,6 +25,8 @@ mod data; mod diff; mod flow_model; mod fmt; +#[cfg(feature = "item_interactions")] +mod item_model; mod params; mod resource_rt; mod rt; diff --git a/workspace_tests/src/mock_item.rs b/workspace_tests/src/mock_item.rs index af0668ca4..ca71c61ff 100644 --- a/workspace_tests/src/mock_item.rs +++ b/workspace_tests/src/mock_item.rs @@ -6,7 +6,10 @@ use std::{ }; #[cfg(feature = "output_progress")] -use peace::cfg::progress::{ProgressLimit, ProgressMsgUpdate}; +use peace::{ + cfg::progress::{ProgressLimit, ProgressMsgUpdate}, + item_model::ItemLocationState, +}; use peace::{ cfg::{async_trait, item_id, ApplyCheck, FnCtx, Item, ItemId}, data::{ @@ -199,6 +202,11 @@ where &self.id } + #[cfg(feature = "item_state_example")] + fn state_example(params: &Self::Params<'_>, _data: Self::Data<'_>) -> Self::State { + MockState(params.0) + } + async fn state_clean( params_partial: & as Params>::Partial, data: Self::Data<'_>, @@ -364,6 +372,16 @@ where resources.insert(mock_dest); Ok(()) } + + #[cfg(feature = "item_interactions")] + fn interactions( + _params: &Self::Params<'_>, + _data: Self::Data<'_>, + ) -> Vec { + use peace::item_model::{ItemInteractionWithin, ItemLocation}; + + vec![ItemInteractionWithin::new(vec![ItemLocation::localhost()].into()).into()] + } } #[cfg(feature = "error_reporting")] @@ -491,6 +509,13 @@ impl fmt::Display for MockState { } } +#[cfg(feature = "output_progress")] +impl<'state> From<&'state MockState> for ItemLocationState { + fn from(_mock_state: &'state MockState) -> ItemLocationState { + ItemLocationState::Exists + } +} + #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct MockDiff(pub i16); diff --git a/workspace_tests/src/no_op_output.rs b/workspace_tests/src/no_op_output.rs index e7cec2cee..b663748de 100644 --- a/workspace_tests/src/no_op_output.rs +++ b/workspace_tests/src/no_op_output.rs @@ -3,7 +3,11 @@ use peace::{cfg::async_trait, fmt::Presentable, rt_model::output::OutputWrite}; cfg_if::cfg_if! { if #[cfg(feature = "output_progress")] { use peace::{ - cfg::progress::{ProgressTracker, ProgressUpdateAndId}, + cfg::{ + progress::{CmdBlockItemInteractionType, ProgressTracker, ProgressUpdateAndId}, + ItemId, + }, + item_model::ItemLocationState, rt_model::CmdProgressTracker, }; } @@ -21,6 +25,21 @@ where #[cfg(feature = "output_progress")] async fn progress_begin(&mut self, _cmd_progress_tracker: &CmdProgressTracker) {} + #[cfg(feature = "output_progress")] + async fn cmd_block_start( + &mut self, + _cmd_block_item_interaction_type: CmdBlockItemInteractionType, + ) { + } + + #[cfg(feature = "output_progress")] + async fn item_location_state( + &mut self, + _item_id: ItemId, + _item_location_state: ItemLocationState, + ) { + } + #[cfg(feature = "output_progress")] async fn progress_update( &mut self, diff --git a/workspace_tests/src/params/params_spec.rs b/workspace_tests/src/params/params_spec.rs index 87894f9ba..3d5044ac9 100644 --- a/workspace_tests/src/params/params_spec.rs +++ b/workspace_tests/src/params/params_spec.rs @@ -346,12 +346,10 @@ fn is_usable_returns_false_for_stored() { #[test] fn is_usable_returns_true_for_value_and_in_memory() { - assert!( - ParamsSpec::::Value { - value: VecA::default() - } - .is_usable() - ); + assert!(ParamsSpec::::Value { + value: VecA::default() + } + .is_usable()); assert!(ParamsSpec::::InMemory.is_usable()); } diff --git a/workspace_tests/src/rt/cmds/clean_cmd.rs b/workspace_tests/src/rt/cmds/clean_cmd.rs index 852340c92..85adae141 100644 --- a/workspace_tests/src/rt/cmds/clean_cmd.rs +++ b/workspace_tests/src/rt/cmds/clean_cmd.rs @@ -21,8 +21,8 @@ use crate::{ }; #[tokio::test] -async fn resources_cleaned_dry_does_not_alter_state_when_state_not_ensured() --> Result<(), Box> { +async fn resources_cleaned_dry_does_not_alter_state_when_state_not_ensured( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -43,7 +43,7 @@ async fn resources_cleaned_dry_does_not_alter_state_when_state_not_ensured() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -88,8 +88,8 @@ async fn resources_cleaned_dry_does_not_alter_state_when_state_not_ensured() } #[tokio::test] -async fn resources_cleaned_dry_does_not_alter_state_when_state_ensured() --> Result<(), Box> { +async fn resources_cleaned_dry_does_not_alter_state_when_state_ensured( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -110,7 +110,7 @@ async fn resources_cleaned_dry_does_not_alter_state_when_state_ensured() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -191,8 +191,8 @@ async fn resources_cleaned_dry_does_not_alter_state_when_state_ensured() } #[tokio::test] -async fn resources_cleaned_contains_state_cleaned_for_each_item_when_state_not_ensured() --> Result<(), Box> { +async fn resources_cleaned_contains_state_cleaned_for_each_item_when_state_not_ensured( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -213,7 +213,7 @@ async fn resources_cleaned_contains_state_cleaned_for_each_item_when_state_not_e (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -253,8 +253,8 @@ async fn resources_cleaned_contains_state_cleaned_for_each_item_when_state_not_e } #[tokio::test] -async fn resources_cleaned_contains_state_cleaned_for_each_item_when_state_ensured() --> Result<(), Box> { +async fn resources_cleaned_contains_state_cleaned_for_each_item_when_state_ensured( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -275,7 +275,7 @@ async fn resources_cleaned_contains_state_cleaned_for_each_item_when_state_ensur (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -328,8 +328,8 @@ async fn resources_cleaned_contains_state_cleaned_for_each_item_when_state_ensur } #[tokio::test] -async fn exec_dry_returns_sync_error_when_current_state_out_of_sync() --> Result<(), Box> { +async fn exec_dry_returns_sync_error_when_current_state_out_of_sync( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -350,7 +350,7 @@ async fn exec_dry_returns_sync_error_when_current_state_out_of_sync() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3]).into(), @@ -374,7 +374,7 @@ async fn exec_dry_returns_sync_error_when_current_state_out_of_sync() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -441,8 +441,8 @@ async fn exec_dry_returns_sync_error_when_current_state_out_of_sync() /// This should not return an error, because the target state for cleaning is /// not `state_goal`, but `state_clean`. #[tokio::test] -async fn exec_dry_does_not_return_sync_error_when_goal_state_out_of_sync() --> Result<(), Box> { +async fn exec_dry_does_not_return_sync_error_when_goal_state_out_of_sync( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -463,7 +463,7 @@ async fn exec_dry_does_not_return_sync_error_when_goal_state_out_of_sync() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3]).into(), @@ -487,7 +487,7 @@ async fn exec_dry_does_not_return_sync_error_when_goal_state_out_of_sync() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -556,8 +556,8 @@ async fn exec_dry_does_not_return_sync_error_when_goal_state_out_of_sync() } #[tokio::test] -async fn exec_returns_sync_error_when_current_state_out_of_sync() --> Result<(), Box> { +async fn exec_returns_sync_error_when_current_state_out_of_sync( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -578,7 +578,7 @@ async fn exec_returns_sync_error_when_current_state_out_of_sync() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3]).into(), @@ -602,7 +602,7 @@ async fn exec_returns_sync_error_when_current_state_out_of_sync() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -668,8 +668,8 @@ async fn exec_returns_sync_error_when_current_state_out_of_sync() /// This should not return an error, because the target state for cleaning is /// not `state_goal`, but `state_clean`. #[tokio::test] -async fn exec_does_not_return_sync_error_when_goal_state_out_of_sync() --> Result<(), Box> { +async fn exec_does_not_return_sync_error_when_goal_state_out_of_sync( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -690,7 +690,7 @@ async fn exec_does_not_return_sync_error_when_goal_state_out_of_sync() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3]).into(), @@ -714,7 +714,7 @@ async fn exec_does_not_return_sync_error_when_goal_state_out_of_sync() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -783,8 +783,8 @@ async fn exec_does_not_return_sync_error_when_goal_state_out_of_sync() } #[tokio::test] -async fn states_current_not_serialized_on_states_clean_insert_cmd_block_fail() --> Result<(), Box> { +async fn states_current_not_serialized_on_states_clean_insert_cmd_block_fail( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -810,7 +810,7 @@ async fn states_current_not_serialized_on_states_clean_insert_cmd_block_fail() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -894,8 +894,8 @@ async fn states_current_not_serialized_on_states_clean_insert_cmd_block_fail() } #[tokio::test] -async fn states_current_not_serialized_on_states_discover_cmd_block_fail() --> Result<(), Box> { +async fn states_current_not_serialized_on_states_discover_cmd_block_fail( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -915,7 +915,7 @@ async fn states_current_not_serialized_on_states_discover_cmd_block_fail() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -963,7 +963,7 @@ async fn states_current_not_serialized_on_states_discover_cmd_block_fail() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .await?; let CmdOutcome::ItemError { diff --git a/workspace_tests/src/rt/cmds/diff_cmd.rs b/workspace_tests/src/rt/cmds/diff_cmd.rs index 59c6ab3dd..925d8162c 100644 --- a/workspace_tests/src/rt/cmds/diff_cmd.rs +++ b/workspace_tests/src/rt/cmds/diff_cmd.rs @@ -44,7 +44,7 @@ async fn diff_stored_contains_state_diff_for_each_item() -> Result<(), Box( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -118,7 +118,7 @@ async fn diff_discover_current_on_demand() -> Result<(), Box( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -196,7 +196,7 @@ async fn diff_discover_goal_on_demand() -> Result<(), Box (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -274,7 +274,7 @@ async fn diff_discover_current_and_goal_on_demand() -> Result<(), Box( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -348,7 +348,7 @@ async fn diff_stored_with_multiple_profiles() -> Result<(), Box( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -372,7 +372,7 @@ async fn diff_stored_with_multiple_profiles() -> Result<(), Box( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -395,7 +395,7 @@ async fn diff_stored_with_multiple_profiles() -> Result<(), Box Result<(), Box( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -488,7 +488,7 @@ async fn diff_stored_with_missing_profile_0() -> Result<(), Box Result<(), Box( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -542,7 +542,7 @@ async fn diff_stored_with_missing_profile_1() -> Result<(), Box Result<(), Box Result<(), Box> { +async fn diff_stored_with_profile_0_missing_states_current( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -581,7 +581,7 @@ async fn diff_stored_with_profile_0_missing_states_current() (&workspace).into(), ) .with_profile(profile_0.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -596,7 +596,7 @@ async fn diff_stored_with_profile_0_missing_states_current() (&workspace).into(), ) .with_profile(profile_1.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -611,7 +611,7 @@ async fn diff_stored_with_profile_0_missing_states_current() output.into(), (&workspace).into(), ) - .with_flow(&flow) + .with_flow((&flow).into()) .await?; let diff_result = @@ -628,8 +628,8 @@ async fn diff_stored_with_profile_0_missing_states_current() } #[tokio::test] -async fn diff_stored_with_profile_1_missing_states_current() --> Result<(), Box> { +async fn diff_stored_with_profile_1_missing_states_current( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -650,7 +650,7 @@ async fn diff_stored_with_profile_1_missing_states_current() (&workspace).into(), ) .with_profile(profile_0.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -665,7 +665,7 @@ async fn diff_stored_with_profile_1_missing_states_current() (&workspace).into(), ) .with_profile(profile_1.clone()) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -678,7 +678,7 @@ async fn diff_stored_with_profile_1_missing_states_current() output.into(), (&workspace).into(), ) - .with_flow(&flow) + .with_flow((&flow).into()) .await?; let diff_result = @@ -716,14 +716,12 @@ async fn diff_with_multiple_changes() -> Result<(), Box> CliOutput<&mut Vec>, >(output.into(), (&workspace).into()) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) + // overwrite initial state + .with_resource(VecA(vec![0, 1, 2, 4, 5, 6, 8, 9])) + .with_resource(VecB(vec![0, 1, 2, 3, 4, 5, 6, 7])) .with_item_params::(VecCopyItem::ID_DEFAULT.clone(), ParamsSpec::InMemory) .await?; - // overwrite initial state - let resources = cmd_ctx.resources_mut(); - #[rustfmt::skip] - resources.insert(VecA(vec![0, 1, 2, 4, 5, 6, 8, 9])); - resources.insert(VecB(vec![0, 1, 2, 3, 4, 5, 6, 7])); let CmdOutcome::Complete { value: (states_current, states_goal), cmd_blocks_processed: _, diff --git a/workspace_tests/src/rt/cmds/ensure_cmd.rs b/workspace_tests/src/rt/cmds/ensure_cmd.rs index b7648180f..d3e2ca180 100644 --- a/workspace_tests/src/rt/cmds/ensure_cmd.rs +++ b/workspace_tests/src/rt/cmds/ensure_cmd.rs @@ -46,7 +46,7 @@ async fn resources_ensured_dry_does_not_alter_state() -> Result<(), Box( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -97,8 +97,8 @@ async fn resources_ensured_dry_does_not_alter_state() -> Result<(), Box Result<(), Box> { +async fn resources_ensured_contains_state_ensured_for_each_item_when_state_not_yet_ensured( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -119,7 +119,7 @@ async fn resources_ensured_contains_state_ensured_for_each_item_when_state_not_y (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -135,7 +135,7 @@ async fn resources_ensured_contains_state_ensured_for_each_item_when_state_not_y (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .await?; let CmdOutcome::Complete { value: states_ensured, @@ -152,7 +152,7 @@ async fn resources_ensured_contains_state_ensured_for_each_item_when_state_not_y (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .await?; let CmdOutcome::Complete { value: states_current_stored, @@ -201,8 +201,8 @@ async fn resources_ensured_contains_state_ensured_for_each_item_when_state_not_y } #[tokio::test] -async fn resources_ensured_contains_state_ensured_for_each_item_when_state_already_ensured() --> Result<(), Box> { +async fn resources_ensured_contains_state_ensured_for_each_item_when_state_already_ensured( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -223,7 +223,7 @@ async fn resources_ensured_contains_state_ensured_for_each_item_when_state_alrea (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -248,7 +248,7 @@ async fn resources_ensured_contains_state_ensured_for_each_item_when_state_alrea (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3]).into(), @@ -272,7 +272,7 @@ async fn resources_ensured_contains_state_ensured_for_each_item_when_state_alrea (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -334,8 +334,8 @@ async fn resources_ensured_contains_state_ensured_for_each_item_when_state_alrea } #[tokio::test] -async fn exec_dry_returns_sync_error_when_current_state_out_of_sync() --> Result<(), Box> { +async fn exec_dry_returns_sync_error_when_current_state_out_of_sync( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -356,7 +356,7 @@ async fn exec_dry_returns_sync_error_when_current_state_out_of_sync() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3]).into(), @@ -380,7 +380,7 @@ async fn exec_dry_returns_sync_error_when_current_state_out_of_sync() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -445,8 +445,8 @@ async fn exec_dry_returns_sync_error_when_current_state_out_of_sync() } #[tokio::test] -async fn exec_dry_returns_sync_error_when_goal_state_out_of_sync() --> Result<(), Box> { +async fn exec_dry_returns_sync_error_when_goal_state_out_of_sync( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -467,7 +467,7 @@ async fn exec_dry_returns_sync_error_when_goal_state_out_of_sync() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3]).into(), @@ -491,7 +491,7 @@ async fn exec_dry_returns_sync_error_when_goal_state_out_of_sync() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -568,8 +568,8 @@ async fn exec_dry_returns_sync_error_when_goal_state_out_of_sync() } #[tokio::test] -async fn exec_returns_sync_error_when_current_state_out_of_sync() --> Result<(), Box> { +async fn exec_returns_sync_error_when_current_state_out_of_sync( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -590,7 +590,7 @@ async fn exec_returns_sync_error_when_current_state_out_of_sync() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3]).into(), @@ -614,7 +614,7 @@ async fn exec_returns_sync_error_when_current_state_out_of_sync() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -678,8 +678,8 @@ async fn exec_returns_sync_error_when_current_state_out_of_sync() } #[tokio::test] -async fn exec_returns_sync_error_when_goal_state_out_of_sync() --> Result<(), Box> { +async fn exec_returns_sync_error_when_goal_state_out_of_sync( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -700,7 +700,7 @@ async fn exec_returns_sync_error_when_goal_state_out_of_sync() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3]).into(), @@ -724,7 +724,7 @@ async fn exec_returns_sync_error_when_goal_state_out_of_sync() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -801,8 +801,8 @@ async fn exec_returns_sync_error_when_goal_state_out_of_sync() } #[tokio::test] -async fn exec_dry_returns_item_error_when_item_discover_current_returns_error() --> Result<(), Box> { +async fn exec_dry_returns_item_error_when_item_discover_current_returns_error( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -823,7 +823,7 @@ async fn exec_dry_returns_item_error_when_item_discover_current_returns_error() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3]).into(), @@ -861,7 +861,7 @@ async fn exec_dry_returns_item_error_when_item_discover_current_returns_error() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -915,8 +915,8 @@ async fn exec_dry_returns_item_error_when_item_discover_current_returns_error() } #[tokio::test] -async fn exec_dry_returns_item_error_when_item_discover_goal_returns_error() --> Result<(), Box> { +async fn exec_dry_returns_item_error_when_item_discover_goal_returns_error( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -937,7 +937,7 @@ async fn exec_dry_returns_item_error_when_item_discover_goal_returns_error() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3]).into(), @@ -975,7 +975,7 @@ async fn exec_dry_returns_item_error_when_item_discover_goal_returns_error() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -1029,8 +1029,8 @@ async fn exec_dry_returns_item_error_when_item_discover_goal_returns_error() } #[tokio::test] -async fn exec_dry_returns_item_error_when_item_apply_check_returns_error() --> Result<(), Box> { +async fn exec_dry_returns_item_error_when_item_apply_check_returns_error( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -1051,7 +1051,7 @@ async fn exec_dry_returns_item_error_when_item_apply_check_returns_error() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3]).into(), @@ -1089,7 +1089,7 @@ async fn exec_dry_returns_item_error_when_item_apply_check_returns_error() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -1143,8 +1143,8 @@ async fn exec_dry_returns_item_error_when_item_apply_check_returns_error() } #[tokio::test] -async fn exec_dry_returns_item_error_when_item_apply_dry_returns_error() --> Result<(), Box> { +async fn exec_dry_returns_item_error_when_item_apply_dry_returns_error( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -1165,7 +1165,7 @@ async fn exec_dry_returns_item_error_when_item_apply_dry_returns_error() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3]).into(), @@ -1203,7 +1203,7 @@ async fn exec_dry_returns_item_error_when_item_apply_dry_returns_error() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -1257,8 +1257,8 @@ async fn exec_dry_returns_item_error_when_item_apply_dry_returns_error() } #[tokio::test] -async fn exec_returns_item_error_when_item_apply_returns_error() --> Result<(), Box> { +async fn exec_returns_item_error_when_item_apply_returns_error( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -1279,7 +1279,7 @@ async fn exec_returns_item_error_when_item_apply_returns_error() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3]).into(), @@ -1317,7 +1317,7 @@ async fn exec_returns_item_error_when_item_apply_returns_error() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -1371,8 +1371,8 @@ async fn exec_returns_item_error_when_item_apply_returns_error() } #[tokio::test] -async fn states_current_not_serialized_on_states_current_read_cmd_block_interrupt() --> Result<(), Box> { +async fn states_current_not_serialized_on_states_current_read_cmd_block_interrupt( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -1398,7 +1398,7 @@ async fn states_current_not_serialized_on_states_current_read_cmd_block_interrup InterruptStrategy::FinishCurrent, )) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -1460,8 +1460,8 @@ async fn states_current_not_serialized_on_states_current_read_cmd_block_interrup } #[tokio::test] -async fn states_current_not_serialized_on_states_goal_read_cmd_block_interrupt() --> Result<(), Box> { +async fn states_current_not_serialized_on_states_goal_read_cmd_block_interrupt( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -1487,7 +1487,7 @@ async fn states_current_not_serialized_on_states_goal_read_cmd_block_interrupt() InterruptStrategy::PollNextN(2), )) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -1551,8 +1551,8 @@ async fn states_current_not_serialized_on_states_goal_read_cmd_block_interrupt() } #[tokio::test] -async fn states_current_not_serialized_on_states_discover_cmd_block_fail() --> Result<(), Box> { +async fn states_current_not_serialized_on_states_discover_cmd_block_fail( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -1572,7 +1572,7 @@ async fn states_current_not_serialized_on_states_discover_cmd_block_fail() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -1613,7 +1613,7 @@ async fn states_current_not_serialized_on_states_discover_cmd_block_fail() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .await?; let CmdOutcome::ItemError { @@ -1680,8 +1680,8 @@ async fn states_current_not_serialized_on_states_discover_cmd_block_fail() } #[tokio::test] -async fn states_current_not_serialized_on_apply_state_sync_check_cmd_block_interrupt() --> Result<(), Box> { +async fn states_current_not_serialized_on_apply_state_sync_check_cmd_block_interrupt( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -1707,7 +1707,7 @@ async fn states_current_not_serialized_on_apply_state_sync_check_cmd_block_inter InterruptStrategy::PollNextN(7), )) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -1783,8 +1783,8 @@ async fn states_current_not_serialized_on_apply_state_sync_check_cmd_block_inter } #[tokio::test] -async fn states_current_is_serialized_on_apply_exec_cmd_block_interrupt() --> Result<(), Box> { +async fn states_current_is_serialized_on_apply_exec_cmd_block_interrupt( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -1811,7 +1811,7 @@ async fn states_current_is_serialized_on_apply_exec_cmd_block_interrupt() InterruptStrategy::PollNextN(9), )) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), diff --git a/workspace_tests/src/rt/cmds/states_current_read_cmd.rs b/workspace_tests/src/rt/cmds/states_current_read_cmd.rs index 8d8d7e176..766efc083 100644 --- a/workspace_tests/src/rt/cmds/states_current_read_cmd.rs +++ b/workspace_tests/src/rt/cmds/states_current_read_cmd.rs @@ -12,8 +12,8 @@ use crate::{ }; #[tokio::test] -async fn reads_states_current_stored_from_disk_when_present() --> Result<(), Box> { +async fn reads_states_current_stored_from_disk_when_present( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -33,7 +33,7 @@ async fn reads_states_current_stored_from_disk_when_present() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -54,7 +54,7 @@ async fn reads_states_current_stored_from_disk_when_present() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -97,7 +97,7 @@ async fn returns_error_when_states_not_on_disk() -> Result<(), Box( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), diff --git a/workspace_tests/src/rt/cmds/states_current_stored_display_cmd.rs b/workspace_tests/src/rt/cmds/states_current_stored_display_cmd.rs index 2eb0c91bb..603379ea0 100644 --- a/workspace_tests/src/rt/cmds/states_current_stored_display_cmd.rs +++ b/workspace_tests/src/rt/cmds/states_current_stored_display_cmd.rs @@ -12,8 +12,8 @@ use crate::{ }; #[tokio::test] -async fn reads_states_current_stored_from_disk_when_present() --> Result<(), Box> { +async fn reads_states_current_stored_from_disk_when_present( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -34,7 +34,7 @@ async fn reads_states_current_stored_from_disk_when_present() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -55,7 +55,7 @@ async fn reads_states_current_stored_from_disk_when_present() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -110,7 +110,7 @@ async fn returns_error_when_states_not_on_disk() -> Result<(), Box( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), diff --git a/workspace_tests/src/rt/cmds/states_discover_cmd.rs b/workspace_tests/src/rt/cmds/states_discover_cmd.rs index 50c2e132b..8706c9677 100644 --- a/workspace_tests/src/rt/cmds/states_discover_cmd.rs +++ b/workspace_tests/src/rt/cmds/states_discover_cmd.rs @@ -22,8 +22,8 @@ use crate::{ use peace::cfg::progress::{ProgressComplete, ProgressStatus}; #[tokio::test] -async fn current_and_goal_discovers_both_states_current_and_goal() --> Result<(), Box> { +async fn current_and_goal_discovers_both_states_current_and_goal( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -42,7 +42,7 @@ async fn current_and_goal_discovers_both_states_current_and_goal() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -141,7 +141,7 @@ async fn current_runs_state_current_for_each_item() -> Result<(), Box( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -174,8 +174,8 @@ async fn current_runs_state_current_for_each_item() -> Result<(), Box Result<(), Box> { +async fn current_inserts_states_current_stored_from_states_current_file( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -193,7 +193,7 @@ async fn current_inserts_states_current_stored_from_states_current_file() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -212,7 +212,7 @@ async fn current_inserts_states_current_stored_from_states_current_file() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -228,7 +228,7 @@ async fn current_inserts_states_current_stored_from_states_current_file() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -254,8 +254,8 @@ async fn current_inserts_states_current_stored_from_states_current_file() } #[tokio::test] -async fn current_returns_error_when_try_state_current_returns_error() --> Result<(), Box> { +async fn current_returns_error_when_try_state_current_returns_error( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -280,7 +280,7 @@ async fn current_returns_error_when_try_state_current_returns_error() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -333,8 +333,8 @@ async fn current_returns_error_when_try_state_current_returns_error() } #[tokio::test] -async fn goal_returns_error_when_try_state_goal_returns_error() --> Result<(), Box> { +async fn goal_returns_error_when_try_state_goal_returns_error( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -359,7 +359,7 @@ async fn goal_returns_error_when_try_state_goal_returns_error() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -415,8 +415,8 @@ async fn goal_returns_error_when_try_state_goal_returns_error() } #[tokio::test] -async fn current_and_goal_returns_error_when_try_state_current_returns_error() --> Result<(), Box> { +async fn current_and_goal_returns_error_when_try_state_current_returns_error( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -441,7 +441,7 @@ async fn current_and_goal_returns_error_when_try_state_current_returns_error() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -522,8 +522,8 @@ async fn current_and_goal_returns_error_when_try_state_current_returns_error() } #[tokio::test] -async fn current_and_goal_returns_error_when_try_state_goal_returns_error() --> Result<(), Box> { +async fn current_and_goal_returns_error_when_try_state_goal_returns_error( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -548,7 +548,7 @@ async fn current_and_goal_returns_error_when_try_state_goal_returns_error() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -628,8 +628,8 @@ async fn current_and_goal_returns_error_when_try_state_goal_returns_error() } #[tokio::test] -async fn current_and_goal_returns_current_error_when_both_try_state_current_and_try_state_goal_return_error() --> Result<(), Box> { +async fn current_and_goal_returns_current_error_when_both_try_state_current_and_try_state_goal_return_error( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -657,7 +657,7 @@ async fn current_and_goal_returns_current_error_when_both_try_state_current_and_ (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -748,7 +748,7 @@ async fn goal_runs_state_goal_for_each_item() -> Result<(), Box( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -788,8 +788,8 @@ async fn goal_runs_state_goal_for_each_item() -> Result<(), Box Result<(), Box> { +async fn current_with_does_not_serialize_states_when_told_not_to( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -807,7 +807,7 @@ async fn current_with_does_not_serialize_states_when_told_not_to() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3]).into(), @@ -830,7 +830,7 @@ async fn current_with_does_not_serialize_states_when_told_not_to() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .await?; // Overwrite states current. cmd_ctx @@ -870,8 +870,8 @@ async fn current_with_does_not_serialize_states_when_told_not_to() } #[tokio::test] -async fn goal_with_does_not_serialize_states_when_told_not_to() --> Result<(), Box> { +async fn goal_with_does_not_serialize_states_when_told_not_to( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -889,7 +889,7 @@ async fn goal_with_does_not_serialize_states_when_told_not_to() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3]).into(), @@ -908,7 +908,7 @@ async fn goal_with_does_not_serialize_states_when_told_not_to() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -949,8 +949,8 @@ async fn goal_with_does_not_serialize_states_when_told_not_to() #[cfg(feature = "output_progress")] #[tokio::test] -async fn current_with_sets_progress_complete_for_successful_items() --> Result<(), Box> { +async fn current_with_sets_progress_complete_for_successful_items( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -968,7 +968,7 @@ async fn current_with_sets_progress_complete_for_successful_items() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -1009,8 +1009,8 @@ async fn current_with_sets_progress_complete_for_successful_items() #[cfg(feature = "output_progress")] #[tokio::test] -async fn goal_with_sets_progress_complete_for_successful_items() --> Result<(), Box> { +async fn goal_with_sets_progress_complete_for_successful_items( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -1028,7 +1028,7 @@ async fn goal_with_sets_progress_complete_for_successful_items() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -1069,8 +1069,8 @@ async fn goal_with_sets_progress_complete_for_successful_items() #[cfg(feature = "output_progress")] #[tokio::test] -async fn current_and_goal_with_sets_progress_complete_for_successful_items() --> Result<(), Box> { +async fn current_and_goal_with_sets_progress_complete_for_successful_items( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let workspace = Workspace::new( app_name!(), @@ -1088,7 +1088,7 @@ async fn current_and_goal_with_sets_progress_complete_for_successful_items() (&workspace).into(), ) .with_profile(profile!("test_profile")) - .with_flow(&flow) + .with_flow((&flow).into()) .with_item_params::( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), diff --git a/workspace_tests/src/rt/cmds/states_goal_display_cmd.rs b/workspace_tests/src/rt/cmds/states_goal_display_cmd.rs index 01cc9add4..23a596b40 100644 --- a/workspace_tests/src/rt/cmds/states_goal_display_cmd.rs +++ b/workspace_tests/src/rt/cmds/states_goal_display_cmd.rs @@ -33,7 +33,7 @@ async fn reads_states_goal_from_disk_when_present() -> Result<(), Box( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -54,7 +54,7 @@ async fn reads_states_goal_from_disk_when_present() -> Result<(), Box( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -107,7 +107,7 @@ async fn returns_error_when_states_not_on_disk() -> Result<(), Box( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), diff --git a/workspace_tests/src/rt/cmds/states_goal_read_cmd.rs b/workspace_tests/src/rt/cmds/states_goal_read_cmd.rs index 8c68a24c5..e7ef0a632 100644 --- a/workspace_tests/src/rt/cmds/states_goal_read_cmd.rs +++ b/workspace_tests/src/rt/cmds/states_goal_read_cmd.rs @@ -32,7 +32,7 @@ async fn reads_states_goal_from_disk_when_present() -> Result<(), Box( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -52,7 +52,7 @@ async fn reads_states_goal_from_disk_when_present() -> Result<(), Box( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), @@ -92,7 +92,7 @@ async fn returns_error_when_states_not_on_disk() -> Result<(), Box( VecCopyItem::ID_DEFAULT.clone(), VecA(vec![0, 1, 2, 3, 4, 5, 6, 7]).into(), diff --git a/workspace_tests/src/rt_model/error.rs b/workspace_tests/src/rt_model/error.rs index 255f92ef1..73f4c8eab 100644 --- a/workspace_tests/src/rt_model/error.rs +++ b/workspace_tests/src/rt_model/error.rs @@ -34,7 +34,7 @@ fn params_specs_mismatch_display_with_all_error_cases() -> fmt::Result { item_id!("params_spec_stored_with_no_item_1"), ParamsSpec::::Stored, ); - Some(params_specs_stored_mismatches) + Box::new(Some(params_specs_stored_mismatches)) }; let params_specs_not_usable = vec![ item_id!("stored_mapping_fn_0"), diff --git a/workspace_tests/src/rt_model/outcomes/item_apply.rs b/workspace_tests/src/rt_model/outcomes/item_apply.rs index a04ba4a77..845ae6c03 100644 --- a/workspace_tests/src/rt_model/outcomes/item_apply.rs +++ b/workspace_tests/src/rt_model/outcomes/item_apply.rs @@ -5,8 +5,8 @@ use peace::{ }; #[test] -fn try_from_returns_ok_when_state_current_stored_is_none_and_others_are_some() --> Result<(), Box> { +fn try_from_returns_ok_when_state_current_stored_is_none_and_others_are_some( +) -> Result<(), Box> { let item_apply_partial = ItemApplyPartial { state_current_stored: None, state_current: Some(123u32), @@ -192,8 +192,8 @@ fn try_from_returns_err_when_apply_check_is_none() -> Result<(), Box Result<(), Box> { +fn item_apply_rt_state_current_stored_returns_state_current_stored( +) -> Result<(), Box> { let item_apply_partial = ItemApplyPartial { state_current_stored: Some(456u32), state_current: Some(123u32), diff --git a/workspace_tests/src/rt_model/outcomes/item_apply_partial.rs b/workspace_tests/src/rt_model/outcomes/item_apply_partial.rs index 5572fbbd7..f89724433 100644 --- a/workspace_tests/src/rt_model/outcomes/item_apply_partial.rs +++ b/workspace_tests/src/rt_model/outcomes/item_apply_partial.rs @@ -5,8 +5,8 @@ use peace::{ }; #[test] -fn item_apply_rt_state_current_stored_returns_state_current_stored() --> Result<(), Box> { +fn item_apply_rt_state_current_stored_returns_state_current_stored( +) -> Result<(), Box> { let item_apply_partial = ItemApplyPartial { state_current_stored: Some(456u32), state_current: Some(123u32), diff --git a/workspace_tests/src/rt_model/outcomes/item_apply_partial_rt.rs b/workspace_tests/src/rt_model/outcomes/item_apply_partial_rt.rs index 89311b4ba..c71b97e94 100644 --- a/workspace_tests/src/rt_model/outcomes/item_apply_partial_rt.rs +++ b/workspace_tests/src/rt_model/outcomes/item_apply_partial_rt.rs @@ -5,8 +5,8 @@ use peace::{ }; #[test] -fn item_apply_rt_state_current_stored_returns_state_current_stored() --> Result<(), Box> { +fn item_apply_rt_state_current_stored_returns_state_current_stored( +) -> Result<(), Box> { let item_apply_partial = ItemApplyPartial { state_current_stored: Some(456u32), state_current: Some(123u32), diff --git a/workspace_tests/src/rt_model/storage.rs b/workspace_tests/src/rt_model/storage.rs index df1a1a94c..ded8da541 100644 --- a/workspace_tests/src/rt_model/storage.rs +++ b/workspace_tests/src/rt_model/storage.rs @@ -54,8 +54,8 @@ async fn serialized_read_returns_t_when_path_exists() -> Result<(), Box Result<(), Box> { +async fn serialized_read_returns_error_when_path_not_exists( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let file_path = tempdir.path().join("t.yaml"); @@ -96,8 +96,8 @@ async fn serialized_read_opt_returns_t_when_path_exists() -> Result<(), Box Result<(), Box> { +async fn serialized_read_opt_returns_none_when_path_not_exists( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let file_path = tempdir.path().join("t.yaml"); @@ -116,8 +116,8 @@ async fn serialized_read_opt_returns_none_when_path_not_exists() } #[tokio::test] -async fn serialized_typemap_read_opt_returns_typemap_when_path_exists() --> Result<(), Box> { +async fn serialized_typemap_read_opt_returns_typemap_when_path_exists( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let file_path = tempdir.path().join("t.yaml"); tokio::fs::write(&file_path, br#"0: { a: 1 }"#).await?; @@ -143,8 +143,8 @@ async fn serialized_typemap_read_opt_returns_typemap_when_path_exists() } #[tokio::test] -async fn serialized_typemap_read_opt_returns_none_when_path_not_exists() --> Result<(), Box> { +async fn serialized_typemap_read_opt_returns_none_when_path_not_exists( +) -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let file_path = tempdir.path().join("t.yaml"); let mut type_reg = TypeReg::new(); diff --git a/workspace_tests/src/rt_model/workspace_dirs_builder.rs b/workspace_tests/src/rt_model/workspace_dirs_builder.rs index c95e92530..db5467a95 100644 --- a/workspace_tests/src/rt_model/workspace_dirs_builder.rs +++ b/workspace_tests/src/rt_model/workspace_dirs_builder.rs @@ -47,8 +47,8 @@ fn returns_workspace_dir_from_first_dir_with_file() -> Result<(), Box Result<(), Box> { +fn returns_workspace_file_not_found_when_workspace_root_file_does_not_exist( +) -> Result<(), Box> { let workspace_dirs_result = WorkspaceDirsBuilder::build( &app_name!(), WorkspaceSpec::FirstDirWithFile("non_existent_file".into()), diff --git a/workspace_tests/src/vec_copy_item.rs b/workspace_tests/src/vec_copy_item.rs index f7d8e2a12..f57da566f 100644 --- a/workspace_tests/src/vec_copy_item.rs +++ b/workspace_tests/src/vec_copy_item.rs @@ -5,7 +5,10 @@ use std::{ use diff::{Diff, VecDiff, VecDiffType}; #[cfg(feature = "output_progress")] -use peace::cfg::progress::{ProgressLimit, ProgressMsgUpdate}; +use peace::{ + cfg::progress::{ProgressLimit, ProgressMsgUpdate}, + item_model::ItemLocationState, +}; use peace::{ cfg::{async_trait, item_id, ApplyCheck, FnCtx, Item, ItemId}, data::{ @@ -91,6 +94,11 @@ impl Item for VecCopyItem { &self.id } + #[cfg(feature = "item_state_example")] + fn state_example(params: &Self::Params<'_>, _data: Self::Data<'_>) -> Self::State { + VecCopyState(params.0.clone()) + } + async fn try_state_current( fn_ctx: FnCtx<'_>, _params_partial: & as Params>::Partial, @@ -150,7 +158,7 @@ impl Item for VecCopyItem { state_target: &Self::State, diff: &Self::StateDiff, ) -> Result { - let apply_check = if diff.0.0.is_empty() { + let apply_check = if diff.0 .0.is_empty() { ApplyCheck::ExecNotRequired } else { #[cfg(not(feature = "output_progress"))] @@ -222,6 +230,30 @@ impl Item for VecCopyItem { resources.insert(vec_b); Ok(()) } + + #[cfg(feature = "item_interactions")] + fn interactions( + _params: &Self::Params<'_>, + _data: Self::Data<'_>, + ) -> Vec { + use peace::item_model::{ItemInteractionPush, ItemLocation}; + + let item_interaction = ItemInteractionPush::new( + vec![ + ItemLocation::localhost(), + ItemLocation::path("Vec A".to_string()), + ] + .into(), + vec![ + ItemLocation::localhost(), + ItemLocation::path("Vec B".to_string()), + ] + .into(), + ) + .into(); + + vec![item_interaction] + } } #[cfg(feature = "error_reporting")] @@ -247,7 +279,7 @@ pub struct VecCopyData<'exec> { dest: W<'exec, VecB>, } -impl<'exec> VecCopyData<'exec> { +impl VecCopyData<'_> { pub fn dest(&self) -> &VecB { &self.dest } @@ -299,6 +331,19 @@ impl fmt::Display for VecCopyState { } } +impl Default for VecCopyState { + fn default() -> Self { + Self::new() + } +} + +#[cfg(feature = "output_progress")] +impl<'state> From<&'state VecCopyState> for ItemLocationState { + fn from(_vec_copy_state: &'state VecCopyState) -> ItemLocationState { + ItemLocationState::Exists + } +} + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct VecCopyDiff(VecDiff); @@ -326,7 +371,7 @@ impl fmt::Display for VecCopyDiff { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "[")?; self.0 - .0 + .0 .iter() .try_for_each(|vec_diff_type| match vec_diff_type { VecDiffType::Removed { index, len } => {