diff --git a/mantle/Cargo.lock b/mantle/Cargo.lock index a41e950..9714281 100644 --- a/mantle/Cargo.lock +++ b/mantle/Cargo.lock @@ -38,6 +38,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "anyhow" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" + [[package]] name = "approx" version = "0.5.1" @@ -71,9 +77,9 @@ version = "0.1.56" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.50", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -411,10 +417,10 @@ dependencies = [ "itoa 0.4.8", "matches", "phf", - "proc-macro2 1.0.39", - "quote 1.0.18", + "proc-macro2 1.0.50", + "quote 1.0.23", "smallvec", - "syn 1.0.96", + "syn 1.0.107", ] [[package]] @@ -423,8 +429,8 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfae75de57f2b2e85e8768c3ea840fd159c8f33e2b6522c7835b7abac81be16e" dependencies = [ - "quote 1.0.18", - "syn 1.0.96", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -433,8 +439,8 @@ version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f877be4f7c9f246b183111634f75baa039715e3f46ce860677d3b19a69fb229c" dependencies = [ - "quote 1.0.18", - "syn 1.0.96", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -459,10 +465,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ "convert_case", - "proc-macro2 1.0.39", - "quote 1.0.18", + "proc-macro2 1.0.50", + "quote 1.0.23", "rustc_version 0.4.0", - "syn 1.0.96", + "syn 1.0.107", +] + +[[package]] +name = "derive_resource" +version = "0.1.0" +dependencies = [ + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -583,6 +597,18 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum_dispatch" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f36e95862220b211a6e2aa5eca09b4fa391b13cd52ceb8035a24bf65a79de2" +dependencies = [ + "once_cell", + "proc-macro2 1.0.50", + "quote 1.0.23", + "syn 1.0.107", +] + [[package]] name = "env_logger" version = "0.9.0" @@ -739,9 +765,9 @@ version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.50", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -916,9 +942,9 @@ dependencies = [ "log", "mac", "markup5ever", - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.50", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -1521,9 +1547,9 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.50", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -1648,9 +1674,9 @@ dependencies = [ "phf_generator 0.8.0", "phf_shared 0.8.0", "proc-macro-hack", - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.50", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -1686,9 +1712,9 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.50", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -1749,14 +1775,14 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "pretty_assertions" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89f989ac94207d048d92db058e4f6ec7342b0971fc58d1271ca148b799b3563" +checksum = "a25e9bcb20aa780fd0bb16b72403a9064d6b3f22f026946029acb941a50af755" dependencies = [ - "ansi_term", "ctor", "diff", "output_vt100", + "yansi", ] [[package]] @@ -1776,9 +1802,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.39" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" +checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" dependencies = [ "unicode-ident", ] @@ -1798,8 +1824,8 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98eee3c112f2a6f784b6713fe1d7fb7d6506e066121c0a49371fdb976f72bae5" dependencies = [ - "quote 1.0.18", - "syn 1.0.96", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -1831,11 +1857,11 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.18" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" dependencies = [ - "proc-macro2 1.0.39", + "proc-macro2 1.0.50", ] [[package]] @@ -2036,13 +2062,17 @@ dependencies = [ name = "rbx_mantle" version = "0.11.6" dependencies = [ + "anyhow", "async-trait", "chrono", "clap", + "derive_resource", "difference", + "enum_dispatch", "glob", "log", "logger", + "pretty_assertions", "rbx_api", "rbx_auth", "rusoto_core", @@ -2405,10 +2435,10 @@ name = "schemars_derive" version = "0.8.8-blake.2" source = "git+https://github.com/blake-mealey/schemars?branch=raw-comments#7c0bf9940c16a894059904a2a0583e35cf224d1e" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", + "proc-macro2 1.0.50", + "quote 1.0.23", "serde_derive_internals", - "syn 1.0.96", + "syn 1.0.107", ] [[package]] @@ -2518,9 +2548,9 @@ version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.50", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -2529,9 +2559,9 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dbab34ca63057a1f15280bdf3c39f2b1eb1b54c17e98360e511637aef7418c6" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.50", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -2551,9 +2581,9 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2ad84e47328a31223de7fed7a4f5087f2d6ddfe586cf3ca25b7a165bc0a5aed" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.50", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -2718,11 +2748,11 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", + "proc-macro2 1.0.50", + "quote 1.0.23", "serde", "serde_derive", - "syn 1.0.96", + "syn 1.0.107", ] [[package]] @@ -2732,13 +2762,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" dependencies = [ "base-x", - "proc-macro2 1.0.39", - "quote 1.0.18", + "proc-macro2 1.0.50", + "quote 1.0.23", "serde", "serde_derive", "serde_json", "sha1", - "syn 1.0.96", + "syn 1.0.107", ] [[package]] @@ -2769,8 +2799,8 @@ checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" dependencies = [ "phf_generator 0.10.0", "phf_shared 0.10.0", - "proc-macro2 1.0.39", - "quote 1.0.18", + "proc-macro2 1.0.50", + "quote 1.0.23", ] [[package]] @@ -2798,12 +2828,12 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.96" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", + "proc-macro2 1.0.50", + "quote 1.0.23", "unicode-ident", ] @@ -2889,9 +2919,9 @@ version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.50", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -2975,10 +3005,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f" dependencies = [ "proc-macro-hack", - "proc-macro2 1.0.39", - "quote 1.0.18", + "proc-macro2 1.0.50", + "quote 1.0.23", "standback", - "syn 1.0.96", + "syn 1.0.107", ] [[package]] @@ -3022,9 +3052,9 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.50", + "quote 1.0.23", + "syn 1.0.107", ] [[package]] @@ -3235,9 +3265,9 @@ dependencies = [ "bumpalo", "lazy_static", "log", - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.50", + "quote 1.0.23", + "syn 1.0.107", "wasm-bindgen-shared", ] @@ -3259,7 +3289,7 @@ version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa" dependencies = [ - "quote 1.0.18", + "quote 1.0.23", "wasm-bindgen-macro-support", ] @@ -3269,9 +3299,9 @@ version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2 1.0.50", + "quote 1.0.23", + "syn 1.0.107", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/mantle/Cargo.toml b/mantle/Cargo.toml index bfac622..3a9fdd9 100644 --- a/mantle/Cargo.toml +++ b/mantle/Cargo.toml @@ -10,5 +10,6 @@ members = [ "rbx_auth", "rbx_cookie", "logger", - "integration_executor" + "integration_executor", + "derive_resource" ] diff --git a/mantle/derive_resource/Cargo.toml b/mantle/derive_resource/Cargo.toml new file mode 100644 index 0000000..1c472c7 --- /dev/null +++ b/mantle/derive_resource/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "derive_resource" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +quote = "1.0.23" +syn = "1.0.107" diff --git a/mantle/derive_resource/src/lib.rs b/mantle/derive_resource/src/lib.rs new file mode 100644 index 0000000..f4b65c6 --- /dev/null +++ b/mantle/derive_resource/src/lib.rs @@ -0,0 +1,186 @@ +extern crate proc_macro; +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, Data, DeriveInput}; + +#[proc_macro_derive(ResourceGroup)] +pub fn derive_resource_group(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + let name = &input.ident; + + let variants: Vec<_> = match &input.data { + Data::Enum(data) => data.variants.iter().collect(), + _ => panic!("expected enum to derive ResourceGroup"), + }; + + let variant_idents = variants.iter().map(|variant| variant.ident.clone()); + let variant_idents2 = variant_idents.clone(); + let variant_idents3 = variant_idents.clone(); + let variant_idents4 = variant_idents.clone(); + let variant_idents5 = variant_idents.clone(); + let variant_idents6 = variant_idents.clone(); + let variant_idents7 = variant_idents.clone(); + + let expanded = quote! { + #[async_trait] + impl ResourceGroup for #name { + fn id(&self) -> &str { + match self { + #(Self::#variant_idents(resource) => &resource.id),* + } + } + + fn has_outputs(&self) -> bool { + match self { + #(Self::#variant_idents2(resource) => resource.outputs.is_some()),* + } + } + + fn dependency_ids(&self) -> Vec<&str> { + match self { + #(Self::#variant_idents3(resource) => resource.dependency_ids()),* + } + } + + fn next( + &self, + previous_graph: &ResourceGraph, + next_graph: &ResourceGraph, + ) -> anyhow::Result { + match self { + #(Self::#variant_idents4(resource) => Ok(Self::#variant_idents4(#variant_idents4::next( + resource, + previous_graph.get(&resource.id), + next_graph.get_many(resource.dependency_ids()), + )?))),* + } + } + + async fn create(&mut self) -> anyhow::Result<()> { + match self { + #(Self::#variant_idents5(resource) => resource.create().await),* + } + } + + async fn update(&mut self) -> anyhow::Result<()> { + match self { + #(Self::#variant_idents6(resource) => resource.update().await),* + } + } + + async fn delete(&mut self) -> anyhow::Result<()> { + match self { + #(Self::#variant_idents7(resource) => resource.delete().await),* + } + } + } + }; + + TokenStream::from(expanded) +} + +#[proc_macro_derive(Resource, attributes(dependency, resource_group))] +pub fn derive_resource(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + let name = &input.ident; + + let data = match &input.data { + Data::Struct(data) => data, + _ => panic!("expected struct to derive Resource"), + }; + + let dependency_fields: Vec<_> = data + .fields + .iter() + .filter_map(|field| { + if field.attrs.iter().any(|a| a.path.is_ident("dependency")) { + let var_name = field.ident.clone().unwrap(); + let field_type = if let syn::Type::Path(path) = &field.ty { + path.path.get_ident().unwrap().clone() + } else { + panic!("expected dependency type to be a type path"); + }; + Some((var_name, field_type)) + } else { + None + } + }) + .collect(); + let dependency_field_idents = dependency_fields + .iter() + .map(|(var_name, _field_type)| var_name); + + let dependency_variables = dependency_fields.iter().map(|(var_name, field_type)| { + quote! { + let mut #var_name: Option<#field_type> = None; + } + }); + + let dependency_matchers = dependency_fields.iter().map(|(var_name, field_type)| { + quote! { + RbxResource::#field_type(resource) => { + #var_name = Some(resource.clone()); + } + } + }); + + let dependency_values = dependency_fields.iter().map(|(var_name, field_type)| { + let field_type_str = field_type.to_string(); + quote! { + #var_name: #var_name.ok_or(anyhow::Error::msg(format!( + "Expected dependency of type {} to be present", + #field_type_str + )))? + } + }); + + let expanded = quote! { + impl Resource for #name { + // TODO: RbxResource should come from a variable/attribute + fn next( + resource: &Self, + previous_resource: Option<&RbxResource>, + dependencies: Vec<&RbxResource> + ) -> anyhow::Result { + #(#dependency_variables)* + + for dependency in dependencies { + match dependency { + #(#dependency_matchers)* + _ => {} + } + } + + let outputs = match previous_resource { + Some(RbxResource::#name(resource)) => { + resource.outputs.clone() + } + Some(_) => { + return anyhow::Result::Err(anyhow::Error::msg(format!( + "Expected previous resource with ID {} to be of the same type", + resource.id + ))) + } + None => None + }; + + Ok(Self { + id: resource.id.clone(), + inputs: resource.inputs.clone(), + outputs, + #(#dependency_values),* + }) + } + + fn dependency_ids(&self) -> Vec<&str> { + vec![ + #(&self.#dependency_field_idents.id),* + ] + } + } + }; + + TokenStream::from(expanded) +} diff --git a/mantle/rbx_mantle/Cargo.toml b/mantle/rbx_mantle/Cargo.toml index 204eb69..55cfa88 100644 --- a/mantle/rbx_mantle/Cargo.toml +++ b/mantle/rbx_mantle/Cargo.toml @@ -16,6 +16,7 @@ include = [ rbx_auth = { path = "../rbx_auth" } rbx_api = { path = "../rbx_api" } logger = { path = "../logger" } +derive_resource = { path = "../derive_resource" } serde_yaml = { version = "0.8" } serde = { version = "1.0", features = ["derive"] } @@ -32,3 +33,8 @@ yansi = "0.5.0" url = { version = "2.2.2", features = ["serde"] } log = "0.4.14" schemars = { version = "=0.8.8-blake.2", git = "https://github.com/blake-mealey/schemars", branch = "raw-comments", features = ["derive", "url", "preserve_order"] } +anyhow = "1.0.68" +enum_dispatch = "0.3.11" + +[dev-dependencies] +pretty_assertions = "1.3.0" diff --git a/mantle/rbx_mantle/src/lib.rs b/mantle/rbx_mantle/src/lib.rs index e095d6e..21c6325 100644 --- a/mantle/rbx_mantle/src/lib.rs +++ b/mantle/rbx_mantle/src/lib.rs @@ -1,5 +1,7 @@ pub mod config; pub mod project; pub mod resource_graph; +pub mod resource_graph_v2; +pub mod resources_v2; pub mod roblox_resource_manager; pub mod state; diff --git a/mantle/rbx_mantle/src/resource_graph_v2/evaluator.rs b/mantle/rbx_mantle/src/resource_graph_v2/evaluator.rs new file mode 100644 index 0000000..d59e382 --- /dev/null +++ b/mantle/rbx_mantle/src/resource_graph_v2/evaluator.rs @@ -0,0 +1,298 @@ +use crate::resources_v2::{RbxResource, ResourceGroup}; + +use super::{evaluator_results::EvaluatorResults, ResourceGraph}; + +pub struct Evaluator<'a> { + previous_graph: &'a ResourceGraph, + desired_graph: &'a ResourceGraph, + next_graph: ResourceGraph, + + results: EvaluatorResults, +} + +impl<'a> Evaluator<'a> { + pub fn new(previous_graph: &'a ResourceGraph, desired_graph: &'a ResourceGraph) -> Self { + Self { + previous_graph, + desired_graph, + next_graph: ResourceGraph::default(), + results: EvaluatorResults::default(), + } + } + + pub async fn evaluate( + &'a mut self, + ) -> anyhow::Result<(&'a EvaluatorResults, &'a ResourceGraph)> { + if !self.results.is_empty() { + return anyhow::Result::Err(anyhow::Error::msg( + "A graph evaluator can only be used once.", + )); + } + + // ensure that both graphs are valid before we attempt to evaluate them + let mut previous_resources = self.previous_graph.topological_order()?; + previous_resources.reverse(); + + let desired_resources = self.desired_graph.topological_order()?; + + self.delete_removed_resources(previous_resources).await; + self.create_or_update_added_or_changed_resources(desired_resources) + .await; + + Ok((&self.results, &self.next_graph)) + } + + async fn delete_removed_resources(&mut self, previous_resources_reverse: Vec<&RbxResource>) { + for resource in previous_resources_reverse.into_iter() { + if self.desired_graph.contains(resource.id()) { + continue; + } + + println!("Deleting: {}", resource.id()); + + let mut next_resource = resource.clone(); + + match next_resource.delete().await { + Ok(()) => self.results.delete_succeeded(resource.id()), + Err(error) => { + self.results.delete_failed(resource.id(), error); + self.next_graph.insert(next_resource); + } + } + } + } + + async fn create_or_update_added_or_changed_resources( + &mut self, + desired_resources: Vec<&RbxResource>, + ) { + for desired_resource in desired_resources.into_iter() { + if let Some(previous_resource) = self.previous_graph.get(desired_resource.id()) { + match desired_resource.next(&self.previous_graph, &self.next_graph) { + Ok(mut next_resource) => { + if *previous_resource == next_resource { + self.results.noop(next_resource.id()); + self.next_graph.insert(next_resource); + } else { + match next_resource.update().await { + Ok(()) => { + self.results.update_succeeded(next_resource.id()); + self.next_graph.insert(next_resource); + } + Err(error) => { + self.results.update_failed(next_resource.id(), error); + self.next_graph.insert(previous_resource.clone()); + } + } + } + } + Err(error) => self.results.update_failed(desired_resource.id(), error), + } + } else { + match desired_resource.next(&self.previous_graph, &self.next_graph) { + Ok(mut next_resource) => match next_resource.create().await { + Ok(()) => { + self.results.create_succeeded(next_resource.id()); + self.next_graph.insert(next_resource); + } + Err(error) => { + self.results.create_failed(next_resource.id(), error); + } + }, + Err(error) => self.results.create_failed(desired_resource.id(), error), + } + } + } + } +} + +#[cfg(test)] +pub mod tests { + use crate::{ + resource_graph_v2::{ + evaluator::Evaluator, + evaluator_results::{ + EvaluatorResults, OperationResult, OperationStatus, OperationType, + }, + ResourceGraph, + }, + resources_v2::{ + experience::{Experience, ExperienceInputs, ExperienceOutputs}, + place::{Place, PlaceInputs, PlaceOutputs}, + RbxResource, + }, + }; + use pretty_assertions::assert_eq; + + #[tokio::test] + pub async fn create_resources() { + let mut desired_graph = ResourceGraph::default(); + let desired_experience = Experience { + id: "experience_singleton".to_owned(), + inputs: ExperienceInputs { group_id: None }, + outputs: None, + }; + let desired_start_place = Place { + id: "place_start".to_owned(), + inputs: PlaceInputs { is_start: true }, + outputs: None, + experience: desired_experience.clone(), + }; + desired_graph.insert(RbxResource::Place(desired_start_place)); + let desired_other_place = Place { + id: "place_other".to_owned(), + inputs: PlaceInputs { is_start: false }, + outputs: None, + experience: desired_experience.clone(), + }; + desired_graph.insert(RbxResource::Place(desired_other_place)); + desired_graph.insert(RbxResource::Experience(desired_experience)); + + let previous_graph = ResourceGraph::default(); + + let mut evaluator = Evaluator::new(&previous_graph, &desired_graph); + let (results, _next_graph) = evaluator.evaluate().await.unwrap(); + + dbg!(_next_graph); + + assert_eq!( + *results, + EvaluatorResults { + operation_results: vec![ + OperationResult { + resource_id: "experience_singleton".to_owned(), + operation_type: OperationType::Create, + status: OperationStatus::Success + }, + OperationResult { + resource_id: "place_start".to_owned(), + operation_type: OperationType::Create, + status: OperationStatus::Success + }, + OperationResult { + resource_id: "place_other".to_owned(), + operation_type: OperationType::Create, + status: OperationStatus::Success + } + ] + } + ); + } + + #[tokio::test] + pub async fn update_resources_noop() { + let mut previous_graph = ResourceGraph::default(); + let previous_experience = Experience { + id: "experience_singleton".to_owned(), + inputs: ExperienceInputs { group_id: None }, + outputs: Some(ExperienceOutputs { + asset_id: 1, + start_place_id: 2, + }), + }; + let previous_start_place = Place { + id: "place_start".to_owned(), + inputs: PlaceInputs { is_start: true }, + outputs: Some(PlaceOutputs { asset_id: 2 }), + experience: previous_experience.clone(), + }; + previous_graph.insert(RbxResource::Place(previous_start_place)); + previous_graph.insert(RbxResource::Experience(previous_experience)); + + let mut desired_graph = ResourceGraph::default(); + let desired_experience = Experience { + id: "experience_singleton".to_owned(), + inputs: ExperienceInputs { group_id: None }, + outputs: None, + }; + let desired_start_place = Place { + id: "place_start".to_owned(), + inputs: PlaceInputs { is_start: true }, + outputs: None, + experience: desired_experience.clone(), + }; + desired_graph.insert(RbxResource::Place(desired_start_place)); + desired_graph.insert(RbxResource::Experience(desired_experience)); + + let mut evaluator = Evaluator::new(&previous_graph, &desired_graph); + let (results, _next_graph) = evaluator.evaluate().await.unwrap(); + + dbg!(_next_graph); + + assert_eq!( + *results, + EvaluatorResults { + operation_results: vec![ + OperationResult { + resource_id: "experience_singleton".to_owned(), + operation_type: OperationType::Noop, + status: OperationStatus::Success + }, + OperationResult { + resource_id: "place_start".to_owned(), + operation_type: OperationType::Noop, + status: OperationStatus::Success + } + ] + } + ); + } + + #[tokio::test] + pub async fn update_resources_changes() { + let mut previous_graph = ResourceGraph::default(); + let previous_experience = Experience { + id: "experience_singleton".to_owned(), + inputs: ExperienceInputs { group_id: None }, + outputs: None, + }; + let previous_start_place = Place { + id: "place_start".to_owned(), + inputs: PlaceInputs { is_start: true }, + outputs: None, + experience: previous_experience.clone(), + }; + previous_graph.insert(RbxResource::Place(previous_start_place)); + previous_graph.insert(RbxResource::Experience(previous_experience)); + + let mut desired_graph = ResourceGraph::default(); + let desired_experience = Experience { + id: "experience_singleton".to_owned(), + inputs: ExperienceInputs { + group_id: Some(123), + }, + outputs: None, + }; + let desired_start_place = Place { + id: "place_start".to_owned(), + inputs: PlaceInputs { is_start: true }, + outputs: None, + experience: desired_experience.clone(), + }; + desired_graph.insert(RbxResource::Place(desired_start_place)); + desired_graph.insert(RbxResource::Experience(desired_experience)); + + let mut evaluator = Evaluator::new(&previous_graph, &desired_graph); + let (results, _next_graph) = evaluator.evaluate().await.unwrap(); + + dbg!(_next_graph); + + assert_eq!( + *results, + EvaluatorResults { + operation_results: vec![ + OperationResult { + resource_id: "experience_singleton".to_owned(), + operation_type: OperationType::Update, + status: OperationStatus::Success + }, + OperationResult { + resource_id: "place_start".to_owned(), + operation_type: OperationType::Update, + status: OperationStatus::Success + } + ] + } + ); + } +} diff --git a/mantle/rbx_mantle/src/resource_graph_v2/evaluator_results.rs b/mantle/rbx_mantle/src/resource_graph_v2/evaluator_results.rs new file mode 100644 index 0000000..4313463 --- /dev/null +++ b/mantle/rbx_mantle/src/resource_graph_v2/evaluator_results.rs @@ -0,0 +1,124 @@ +#[derive(Debug, PartialEq)] +pub enum SkipReason { + PurchasesNotAllowed, +} + +#[derive(Debug, PartialEq)] +pub enum OperationType { + Create, + Update, + Recreate, + Delete, + Noop, + Skip(SkipReason), +} + +#[derive(Debug)] +pub enum OperationStatus { + Success, + Failure(anyhow::Error), +} +// Ignores error messages +impl PartialEq for OperationStatus { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Success, Self::Success) => true, + (Self::Failure(_), Self::Failure(_)) => true, + _ => false, + } + } +} + +#[derive(Debug, PartialEq)] +pub struct OperationResult { + pub resource_id: String, + pub operation_type: OperationType, + pub status: OperationStatus, +} + +#[derive(Default, Debug, PartialEq)] +pub struct EvaluatorResults { + pub operation_results: Vec, +} + +impl EvaluatorResults { + pub fn is_empty(&self) -> bool { + self.operation_results.is_empty() + } + + pub fn create_succeeded(&mut self, resource_id: &str) { + self.operation_results.push(OperationResult { + resource_id: resource_id.to_owned(), + operation_type: OperationType::Create, + status: OperationStatus::Success, + }) + } + pub fn create_failed(&mut self, resource_id: &str, error: anyhow::Error) { + self.operation_results.push(OperationResult { + resource_id: resource_id.to_owned(), + operation_type: OperationType::Create, + status: OperationStatus::Failure(error), + }) + } + + pub fn update_succeeded(&mut self, resource_id: &str) { + self.operation_results.push(OperationResult { + resource_id: resource_id.to_owned(), + operation_type: OperationType::Update, + status: OperationStatus::Success, + }) + } + pub fn update_failed(&mut self, resource_id: &str, error: anyhow::Error) { + self.operation_results.push(OperationResult { + resource_id: resource_id.to_owned(), + operation_type: OperationType::Update, + status: OperationStatus::Failure(error), + }) + } + + pub fn recreate_succeeded(&mut self, resource_id: &str) { + self.operation_results.push(OperationResult { + resource_id: resource_id.to_owned(), + operation_type: OperationType::Recreate, + status: OperationStatus::Success, + }) + } + pub fn recreate_failed(&mut self, resource_id: &str, error: anyhow::Error) { + self.operation_results.push(OperationResult { + resource_id: resource_id.to_owned(), + operation_type: OperationType::Recreate, + status: OperationStatus::Failure(error), + }) + } + + pub fn delete_succeeded(&mut self, resource_id: &str) { + self.operation_results.push(OperationResult { + resource_id: resource_id.to_owned(), + operation_type: OperationType::Delete, + status: OperationStatus::Success, + }) + } + pub fn delete_failed(&mut self, resource_id: &str, error: anyhow::Error) { + self.operation_results.push(OperationResult { + resource_id: resource_id.to_owned(), + operation_type: OperationType::Delete, + status: OperationStatus::Failure(error), + }) + } + + pub fn noop(&mut self, resource_id: &str) { + self.operation_results.push(OperationResult { + resource_id: resource_id.to_owned(), + operation_type: OperationType::Noop, + status: OperationStatus::Success, + }) + } + + pub fn skip(&mut self, resource_id: &str, reason: SkipReason) { + self.operation_results.push(OperationResult { + resource_id: resource_id.to_owned(), + operation_type: OperationType::Skip(reason), + status: OperationStatus::Success, + }) + } +} diff --git a/mantle/rbx_mantle/src/resource_graph_v2/mod.rs b/mantle/rbx_mantle/src/resource_graph_v2/mod.rs new file mode 100644 index 0000000..77610c8 --- /dev/null +++ b/mantle/rbx_mantle/src/resource_graph_v2/mod.rs @@ -0,0 +1,94 @@ +pub mod evaluator; +pub mod evaluator_results; + +use std::collections::BTreeMap; + +use crate::resources_v2::{RbxResource, ResourceGroup}; + +#[derive(Debug, Default)] +pub struct ResourceGraph { + resources: BTreeMap, +} + +impl ResourceGraph { + pub fn new(resources: Vec) -> Self { + Self { + resources: resources + .into_iter() + .map(|resource| (resource.id().to_owned(), resource)) + .collect(), + } + } + + pub fn contains(&self, resource_id: &str) -> bool { + self.resources.contains_key(resource_id) + } + + pub fn get(&self, resource_id: &str) -> Option<&RbxResource> { + self.resources.get(resource_id) + } + + pub fn get_many(&self, resource_ids: Vec<&str>) -> Vec<&RbxResource> { + resource_ids + .iter() + .filter_map(|id| self.resources.get(*id)) + .collect() + } + + pub fn insert(&mut self, resource: RbxResource) { + self.resources.insert(resource.id().to_owned(), resource); + } + + // TODO: Can we make this less clone-y? Can we use actual resource references? + pub fn topological_order(&self) -> anyhow::Result> { + let mut dependency_graph: BTreeMap> = self + .resources + .iter() + .map(|(id, resource)| { + ( + id.clone(), + resource + .dependency_ids() + .iter() + .map(|d| d.to_owned().to_owned()) + .collect(), + ) + }) + .collect(); + + let mut start_nodes: Vec = dependency_graph + .iter() + .filter_map(|(node, deps)| { + if deps.is_empty() { + Some(node.clone()) + } else { + None + } + }) + .collect(); + + let mut ordered: Vec = Vec::new(); + while let Some(start_node) = start_nodes.pop() { + ordered.push(start_node.clone()); + for (node, deps) in dependency_graph.iter_mut() { + if deps.iter().any(|dep| *dep == start_node) { + deps.retain(|dep| *dep != start_node); + if deps.is_empty() { + start_nodes.push(node.clone()); + } + } + } + } + + let has_cycles = dependency_graph.iter().any(|(_, deps)| !deps.is_empty()); + match has_cycles { + true => Err(anyhow::Error::msg( + "Cannot evaluate resource graph because it has cycles", + )), + false => Ok(ordered + .iter() + .map(|id| self.resources.get(id).unwrap()) + .collect()), + } + } +} diff --git a/mantle/rbx_mantle/src/resources_v2/experience.rs b/mantle/rbx_mantle/src/resources_v2/experience.rs new file mode 100644 index 0000000..dae7425 --- /dev/null +++ b/mantle/rbx_mantle/src/resources_v2/experience.rs @@ -0,0 +1,39 @@ +use async_trait::async_trait; +use derive_resource::Resource; +use rbx_api::models::AssetId; + +use super::{ManagedResource, RbxResource, Resource}; + +#[derive(Debug, Clone, PartialEq)] +pub struct ExperienceInputs { + pub group_id: Option, +} +#[derive(Debug, Clone, PartialEq)] +pub struct ExperienceOutputs { + pub asset_id: AssetId, + pub start_place_id: AssetId, +} +#[derive(Debug, Clone, PartialEq, Resource)] +pub struct Experience { + pub id: String, + pub inputs: ExperienceInputs, + pub outputs: Option, +} + +#[async_trait] +impl ManagedResource for Experience { + async fn create(&mut self) -> anyhow::Result<()> { + self.outputs = Some(ExperienceOutputs { + asset_id: 1, + start_place_id: 2, + }); + Ok(()) + } + async fn update(&mut self) -> anyhow::Result<()> { + Ok(()) + } + async fn delete(&mut self) -> anyhow::Result<()> { + self.outputs = None; + Ok(()) + } +} diff --git a/mantle/rbx_mantle/src/resources_v2/mod.rs b/mantle/rbx_mantle/src/resources_v2/mod.rs new file mode 100644 index 0000000..893f20e --- /dev/null +++ b/mantle/rbx_mantle/src/resources_v2/mod.rs @@ -0,0 +1,51 @@ +pub mod experience; +pub mod place; + +use std::fmt::Debug; + +use async_trait::async_trait; +use derive_resource::ResourceGroup; + +use crate::resource_graph_v2::ResourceGraph; + +use self::{experience::Experience, place::Place}; + +pub trait Resource: Sized { + fn next( + resource: &Self, + previous_resource: Option<&RbxResource>, + dependencies: Vec<&RbxResource>, + ) -> anyhow::Result; + + fn dependency_ids(&self) -> Vec<&str>; +} + +#[derive(Debug, Clone, PartialEq, ResourceGroup)] +pub enum RbxResource { + Experience(Experience), + Place(Place), +} + +#[async_trait] +pub trait ResourceGroup { + fn id(&self) -> &str; + fn has_outputs(&self) -> bool; + fn dependency_ids(&self) -> Vec<&str>; + fn next( + &self, + previous_graph: &ResourceGraph, + next_graph: &ResourceGraph, + ) -> anyhow::Result; + + // TODO: should these be separate somehow? + async fn create(&mut self) -> anyhow::Result<()>; + async fn update(&mut self) -> anyhow::Result<()>; + async fn delete(&mut self) -> anyhow::Result<()>; +} + +#[async_trait] +pub trait ManagedResource { + async fn create(&mut self) -> anyhow::Result<()>; + async fn update(&mut self) -> anyhow::Result<()>; + async fn delete(&mut self) -> anyhow::Result<()>; +} diff --git a/mantle/rbx_mantle/src/resources_v2/place.rs b/mantle/rbx_mantle/src/resources_v2/place.rs new file mode 100644 index 0000000..158ef30 --- /dev/null +++ b/mantle/rbx_mantle/src/resources_v2/place.rs @@ -0,0 +1,43 @@ +use async_trait::async_trait; +use derive_resource::Resource; +use rbx_api::models::AssetId; + +use super::{experience::Experience, ManagedResource, RbxResource, Resource}; + +#[derive(Debug, Clone, PartialEq)] +pub struct PlaceInputs { + pub is_start: bool, +} +#[derive(Debug, Clone, PartialEq)] +pub struct PlaceOutputs { + pub asset_id: AssetId, +} +#[derive(Debug, Clone, PartialEq, Resource)] +pub struct Place { + pub id: String, + pub inputs: PlaceInputs, + pub outputs: Option, + #[dependency] + pub experience: Experience, +} + +#[async_trait] +impl ManagedResource for Place { + async fn create(&mut self) -> anyhow::Result<()> { + if self.inputs.is_start { + self.outputs = Some(PlaceOutputs { + asset_id: self.experience.outputs.as_ref().unwrap().start_place_id, + }) + } else { + self.outputs = Some(PlaceOutputs { asset_id: 3 }); + } + Ok(()) + } + async fn update(&mut self) -> anyhow::Result<()> { + Ok(()) + } + async fn delete(&mut self) -> anyhow::Result<()> { + self.outputs = None; + Ok(()) + } +}