diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3dd5d5ca..79e17aab 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -54,6 +54,8 @@ jobs: run: cargo test --tests --package wasm-rpc-stubgen-tests-integration -- --test-threads=1 --format junit --logfile target/report.xml - name: Build in stub mode run: cargo component build -p golem-wasm-rpc --no-default-features --features stub + - name: Build stubgen without default features + run: cargo build -p golem-wasm-rpc-stubgen --no-default-features - name: Publish Test Report uses: mikepenz/action-junit-report@v4 if: success() || failure() # always run even if the previous step fails diff --git a/Cargo.lock b/Cargo.lock index ee75df75..c1a0a08c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -1921,12 +1921,15 @@ dependencies = [ "heck 0.5.0", "id-arena", "indexmap 2.6.0", + "indoc", "itertools 0.12.1", + "minijinja", "pretty_env_logger", "prettyplease", "proc-macro2", "quote", "regex", + "semver", "serde 1.0.210", "serde_json", "serde_yaml", @@ -1938,7 +1941,8 @@ dependencies = [ "wac-graph", "walkdir", "wit-bindgen-rust 0.26.0", - "wit-parser 0.219.0", + "wit-encoder", + "wit-parser 0.221.2", ] [[package]] @@ -2298,6 +2302,12 @@ dependencies = [ "serde 1.0.210", ] +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + [[package]] name = "inout" version = "0.1.3" @@ -2671,6 +2681,15 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minijinja" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c37e1b517d1dcd0e51dc36c4567b9d5a29262b3ec8da6cb5d35e27a8fb529b5" +dependencies = [ + "serde 1.0.210", +] + [[package]] name = "miniz_oxide" version = "0.8.0" @@ -3300,6 +3319,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "pretty_env_logger" version = "0.5.0" @@ -4401,9 +4430,9 @@ dependencies = [ [[package]] name = "test-r" -version = "0.0.11" +version = "0.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f503de241983649990311e28573ce1e98f62ed65f2b6fb7bdf2496d2ba1c978" +checksum = "5f81c856ebb383e7edb390dd642458f8bc91356760c88df9aab258d8a8d01ecd" dependencies = [ "ctor", "test-r-core", @@ -4413,9 +4442,9 @@ dependencies = [ [[package]] name = "test-r-core" -version = "0.0.10" +version = "0.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf42476bec5da760737467562f6169dc126d3716b74ca072f08246fbcb332000" +checksum = "3cc908dda80def66445eef3560ea54df65cb26b557d43fc1d7fdc3ae5675f31f" dependencies = [ "anstream", "anstyle", @@ -4434,9 +4463,9 @@ dependencies = [ [[package]] name = "test-r-macro" -version = "0.0.11" +version = "0.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "198b6d8536c41236c3bc5dec066dd4d63ff01fab76596a9738d8bf60b050afdb" +checksum = "34e97f6324519d7b08bf4a3cf529f5a14fdce61587771717b7571c6f9af0f458" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -4844,9 +4873,9 @@ checksum = "9dcc60c0624df774c82a0ef104151231d37da4962957d691c011c852b2473314" [[package]] name = "wac-graph" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f708c892ce0ebc06de9915f3da2da9b4e482a8b7d417fa447263b110d0a244" +checksum = "d94268a683b67ae20210565b5f91e106fe05034c36b931e739fe90377ed80b98" dependencies = [ "anyhow", "id-arena", @@ -4863,9 +4892,9 @@ dependencies = [ [[package]] name = "wac-types" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b96fe715180f72ab776d90e8c4f47f8e4297e0e61ab263567a31f73c77d45d8d" +checksum = "f5028a15e266f4c8fed48beb95aebb76af5232dcd554fd849a305a4e5cce1563" dependencies = [ "anyhow", "id-arena", @@ -5265,6 +5294,7 @@ dependencies = [ name = "wasm-rpc-stubgen-tests-integration" version = "0.0.0" dependencies = [ + "assert2", "fs_extra", "golem-wasm-ast", "golem-wasm-rpc-stubgen", @@ -5367,9 +5397,17 @@ version = "0.219.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c771866898879073c53b565a6c7b49953795159836714ac56a5befb581227c5" dependencies = [ - "ahash", "bitflags 2.6.0", - "hashbrown 0.14.5", + "indexmap 2.6.0", +] + +[[package]] +name = "wasmparser" +version = "0.221.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9845c470a2e10b61dd42c385839cdd6496363ed63b5c9e420b5488b77bd22083" +dependencies = [ + "bitflags 2.6.0", "indexmap 2.6.0", "semver", ] @@ -6151,6 +6189,19 @@ dependencies = [ "wit-parser 0.209.1", ] +[[package]] +name = "wit-encoder" +version = "0.221.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c2921dd7e71ae11b6e28b33d42cc0eed4fa6ad3fe3ed1f9c5df80dacfec6a28" +dependencies = [ + "id-arena", + "pretty_assertions", + "semver", + "serde 1.0.210", + "wit-parser 0.221.2", +] + [[package]] name = "wit-parser" version = "0.207.0" @@ -6207,9 +6258,9 @@ dependencies = [ [[package]] name = "wit-parser" -version = "0.219.0" +version = "0.221.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23102e180c0c464f36e293d31a27b524e3ece930d7b5527d2f33f9d2c963de64" +checksum = "fbe1538eea6ea5ddbe5defd0dc82539ad7ba751e1631e9185d24a931f0a5adc8" dependencies = [ "anyhow", "id-arena", @@ -6220,7 +6271,7 @@ dependencies = [ "serde_derive", "serde_json", "unicode-xid", - "wasmparser 0.219.1", + "wasmparser 0.221.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f5fc08dc..37866c7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,8 +17,9 @@ lto = true opt-level = 's' [workspace.dependencies] +assert2 = "0.3.15" fs_extra = "1.3.0" golem-wasm-ast = "1.0.1" tempfile = "3.12.0" -test-r = { version = "0.0.11", default-features = false } +test-r = { version = "0.0.13", default-features = false } tokio = "1.38.0" \ No newline at end of file diff --git a/wasm-rpc-stubgen/Cargo.toml b/wasm-rpc-stubgen/Cargo.toml index 2205e6c7..0a732e28 100644 --- a/wasm-rpc-stubgen/Cargo.toml +++ b/wasm-rpc-stubgen/Cargo.toml @@ -8,7 +8,8 @@ repository = "https://github.com/golemcloud/wasm-rpc" description = "Golem WASM RPC stub generator" [features] -unstable-dec-dep = [] +default = ["app-command"] +app-command = [] [lib] name = "golem_wasm_rpc_stubgen" @@ -30,12 +31,12 @@ harness = false [dependencies] anyhow = "1.0.79" -assert2 = "0.3.15" +assert2 = { workspace = true } +cargo-component = "=0.13.2" +cargo-component-core = "=0.13.2" cargo_toml = "0.20.2" clap = { version = "4.5.7", features = ["derive"] } colored = "2.1.0" -cargo-component-core = "=0.13.2" -cargo-component = "=0.13.2" dir-diff = "0.3.3" fs_extra = { workspace = true } glob = "0.3.1" @@ -44,12 +45,15 @@ golem-wasm-rpc = { path = "../wasm-rpc", version = "0.0.0" } heck = "0.5.0" id-arena = "2.2.1" indexmap = "2.2.6" +indoc = "2.0.5" itertools = "0.12.1" +minijinja = "2.5.0" pretty_env_logger = "0.5.0" prettyplease = "0.2.20" proc-macro2 = "1.0.85" quote = "1.0.36" regex = "1.10.4" +semver = "1.0.23" serde = { version = "1.0.203", features = ["derive"] } serde_json = "1.0.128" serde_yaml = "0.9.33" @@ -57,10 +61,12 @@ syn = "2.0.66" tempfile = { workspace = true } tokio = { workspace = true } toml = "0.8.14" +wac-graph = "=0.6.1" walkdir = "2.5.0" -wac-graph = "0.6.0" wit-bindgen-rust = "=0.26.0" -wit-parser = "=0.219.0" +wit-encoder = "=0.221.2" +wit-parser = "=0.221.2" + [dev-dependencies] test-r = { workspace = true } \ No newline at end of file diff --git a/wasm-rpc-stubgen/example/deps/dep1/dep1.wit b/wasm-rpc-stubgen/example/deps/dep1/dep1.wit deleted file mode 100644 index 120db332..00000000 --- a/wasm-rpc-stubgen/example/deps/dep1/dep1.wit +++ /dev/null @@ -1,30 +0,0 @@ -package test:dep1@0.1.0; - -interface iface2 { - use test:dep2/types@0.1.0.{out}; - - enum a-or-b { - a, - b, - } - - type alias = a-or-b; - - variant storage { - in-memory, - s3(string), - filesystem(filesystem-config) - } - - record filesystem-config { - path: string, - max-size: u64, - } - - g: func(in: a-or-b) -> out; - gg: func(in: a-or-b) -> tuple; - - h: func(s: storage); - hh: func(s: storage) -> a-or-b; - hhh: func(in: alias) -> storage; -} diff --git a/wasm-rpc-stubgen/example/deps/dep2/dep2.wit b/wasm-rpc-stubgen/example/deps/dep2/dep2.wit deleted file mode 100644 index 2de169cb..00000000 --- a/wasm-rpc-stubgen/example/deps/dep2/dep2.wit +++ /dev/null @@ -1,5 +0,0 @@ -package test:dep2@0.1.0; - -interface types { - type out = bool; -} diff --git a/wasm-rpc-stubgen/example/main.wit b/wasm-rpc-stubgen/example/main.wit deleted file mode 100644 index 04a7b5d2..00000000 --- a/wasm-rpc-stubgen/example/main.wit +++ /dev/null @@ -1,77 +0,0 @@ -package test:main; - -interface iface1 { - - flags permissions { - read, - write, - exec, - close - } - - record metadata { - name: string, - origin: string, - perms: permissions - } - - record point { - x: s32, - y: s32, - metadata: metadata - } - - type point-tuple = tuple; - - add: func(x: s32, y: s32) -> s64; - point-to-string: func(p: point) -> string; - points-to-strings: func(ps: list) -> list; - tuple-to-point: func(t: point-tuple, metadata: option) -> result; - - process-result: func(r: result); - new-point: func(x: s32, y: s32, metadata: metadata) -> point; - - get-metadata: func() -> option; - - record product-item { - product-id: string, - name: string, - price: float32, - quantity: u32, - } - - record order { - order-id: string, - items: list, - total: float32, - timestamp: u64, - } - - record order-confirmation { - order-id: string, - } - - variant checkout-result { - error(string), - success(order-confirmation), - } - - resource cart { - constructor(user-id: string); - add-item: func(item: product-item) -> (); - remove-item: func(product-id: string) -> (); - update-item-quantity: func(product-id: string, quantity: u32) -> (); - checkout: func() -> checkout-result; - get-cart-contents: func() -> list; - merge-with: func(other-cart: borrow) -> (); - merge: static func(cart1: borrow, cart2: borrow) -> cart; - } -} - -world api { - export iface1; - export test:dep1/iface2@0.1.0; - - export run: func(param: string); - -} diff --git a/wasm-rpc-stubgen/schema/golem-wasm-rpc.schema.json b/wasm-rpc-stubgen/schema/golem-wasm-rpc.schema.json new file mode 100644 index 00000000..a8186770 --- /dev/null +++ b/wasm-rpc-stubgen/schema/golem-wasm-rpc.schema.json @@ -0,0 +1,292 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "$id": "https://golem.cloud/golem-wasm-rpc.schema.json", + "title": "Golem WASM RPC Application Manifest", + "description": "Golem WASM RPC Application Manifest.", + "type": "object", + "properties": { + "include": { + "type": "array", + "description": "Include paths or globs for searching for application manifest documents. Only allowed in root application manifest documents.", + "items": { + "type": "string" + } + }, + "tempDir": { + "type": "string", + "description": "Temporary directory used for generating and building WIT and WASM artifacts. Default location is golem-temp." + }, + "templates": { + "type": "object", + "description": "Component definition templates", + "additionalProperties": { + "$ref": "#/definitions/componentTemplate" + } + }, + "components": { + "type": "object", + "description": "Components by component names", + "additionalProperties": { + "$ref": "#/definitions/component" + } + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/componentDependency" + } + } + }, + "additionalProperties": false, + "definitions": { + "componentTemplate": { + "oneOf": [ + { + "$ref": "#/definitions/componentProperties", + "additionalProperties": false + }, + { + "$ref": "#/definitions/componentProfiles", + "additionalProperties": false + } + ] + }, + "component": { + "description": "Component definition", + "oneOf": [ + { + "$ref": "#/definitions/componentPropertiesWithTemplateRef", + "additionalProperties": false + }, + { + "$ref": "#/definitions/componentProfilesWithTemplateRef", + "additionalProperties": false + } + ] + }, + "templateRef": { + "properties": { + "template": { + "type": "string", + "description": "Component template to be used for defining this component." + } + }, + "additionalProperties": false + }, + "componentProperties": { + "type": "object", + "properties": { + "sourceWit": { + "type": "string", + "description": "Source WIT directory for the user defined component WIT source(s)." + }, + "generatedWit": { + "type": "string", + "description": "Generated WIT directory created by the golem tooling, which handles exported interface extraction and includes resolved package and stub dependencies." + }, + "componentWasm": { + "type": "string", + "description": "File path for the built WASM component." + }, + "linkedWasm": { + "type": "string", + "description": "File path for the linked WASM component which is ready to be uploaded to Golem." + }, + "build": { + "type": "array", + "description": "Commands used for creating component WASM.", + "items": { + "$ref": "#/definitions/externalCommand" + } + }, + "customCommands": { + "type": "object", + "description": "User defined custom commands.", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/definitions/externalCommand" + } + } + }, + "clean": { + "type": "array", + "description": "User defined extra paths used in the clean command.", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "componentPropertiesWithTemplateRef": { + "type": "object", + "properties": { + "template": { + "type": "string", + "description": "Component template to be used for defining this component." + }, + "sourceWit": { + "type": "string", + "description": "Source WIT directory for the user defined component WIT source(s)." + }, + "generatedWit": { + "type": "string", + "description": "Generated WIT directory created by the golem tooling, which handles exported interface extraction and includes resolved package and stub dependencies." + }, + "componentWasm": { + "type": "string", + "description": "File path for the built WASM component." + }, + "linkedWasm": { + "type": "string", + "description": "File path for the linked WASM component which is ready to be uploaded to Golem." + }, + "build": { + "type": "array", + "description": "Commands used for creating component WASM.", + "items": { + "$ref": "#/definitions/externalCommand" + } + }, + "customCommands": { + "type": "object", + "description": "User defined custom commands.", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/definitions/externalCommand" + } + } + }, + "clean": { + "type": "array", + "description": "User defined extra paths used in the clean command.", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "componentProfiles": { + "type": "object", + "description": "Component definition profiles", + "properties": { + "profiles": { + "type": "object", + "description": "Component definition profiles", + "additionalProperties": { + "$ref": "#/definitions/componentProperties" + } + }, + "defaultProfile": { + "type": "string", + "description": "Default profile" + } + }, + "additionalProperties": false, + "required": [ + "profiles", + "defaultProfile" + ] + }, + "componentProfilesWithTemplateRef": { + "type": "object", + "description": "Component definition profiles", + "properties": { + "template": { + "type": "string", + "description": "Component template to be used for defining this component." + }, + "profiles": { + "type": "object", + "description": "Component definition profiles", + "additionalProperties": { + "$ref": "#/definitions/componentProperties" + } + }, + "defaultProfile": { + "type": "string", + "description": "Default profile" + } + }, + "additionalProperties": false, + "required": [ + "profiles", + "defaultProfile" + ] + }, + "externalCommand": { + "type": "object", + "description": "External command with optional inputs and outputs with up-to-date checks", + "properties": { + }, + "oneOf": [ + { + "properties": { + "command": { + "type": "string", + "description": "External command to execute" + } + }, + "additionalProperties": false, + "required": [ + "command" + ] + }, + { + "properties": { + "command": { + "type": "string", + "description": "External command to execute" + }, + "inputs": { + "type": "array", + "description": "Inputs (paths and globs) for the external command", + "items": { + "type": "string" + } + }, + "outputs": { + "type": "array", + "description": "Output (paths and globs) for the external command", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "required": [ + "command", + "inputs", + "outputs" + ] + } + ] + }, + "componentDependency": { + "type": "object", + "description": "Component dependencies", + "oneOf": [ + { + "properties": { + "type": { + "const": "wasm-rpc", + "description": "WASM RPC dependency" + }, + "target": { + "type": "string", + "description": "Target component name." + } + }, + "required": [ + "type", + "target" + ], + "additionalProperties": false + } + ] + } + } +} \ No newline at end of file diff --git a/wasm-rpc-stubgen/src/cargo.rs b/wasm-rpc-stubgen/src/cargo.rs index 274a2eca..e84beef9 100644 --- a/wasm-rpc-stubgen/src/cargo.rs +++ b/wasm-rpc-stubgen/src/cargo.rs @@ -12,20 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::collections::{BTreeMap, BTreeSet}; -use std::fs; -use std::path::{Path, PathBuf}; - -use crate::commands::log::{log_action, log_warn_action}; -use crate::fs::get_file_name; -use crate::naming; +use crate::fs::PathExtra; +use crate::log::{log_action, log_warn_action, LogColorize}; use crate::stub::StubDefinition; +use crate::wit_resolve::ResolvedWitDir; +use crate::{fs, naming}; +use anyhow::{anyhow, Context}; use cargo_toml::{ Dependency, DependencyDetail, DepsSet, Edition, Inheritable, LtoSetting, Manifest, Profile, - Profiles, StripSetting, + Profiles, StripSetting, Workspace, }; use golem_wasm_rpc::WASM_RPC_VERSION; use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, BTreeSet}; +use std::path::{Path, PathBuf}; use toml::Value; use wit_parser::PackageName; @@ -71,6 +71,10 @@ struct WitDependency { pub fn generate_cargo_toml(def: &StubDefinition) -> anyhow::Result<()> { let mut manifest = Manifest::default(); + if def.config.seal_cargo_workspace { + manifest.workspace = Some(Workspace::default()); + } + let mut wit_dependencies = BTreeMap::new(); wit_dependencies.insert( @@ -87,9 +91,9 @@ pub fn generate_cargo_toml(def: &StubDefinition) -> anyhow::Result<()> { }, ); - let stub_package_name = def.stub_package_name(); - for (dep_package, (dep_package_path, _)) in def.packages_with_wit_sources() { - if dep_package.name == stub_package_name { + let stub_dep_package_ids = def.stub_dep_package_ids(); + for (dep_package_id, dep_package, dep_package_sources) in def.packages_with_wit_sources() { + if !stub_dep_package_ids.contains(&dep_package_id) { continue; } @@ -97,20 +101,18 @@ pub fn generate_cargo_toml(def: &StubDefinition) -> anyhow::Result<()> { wit_dependencies.insert( format_package_name_without_version(&def.source_package_name), WitDependency { - path: naming::wit::package_wit_dep_dir_from_package_name( - &def.source_package_name, - ) - .to_string_lossy() - .to_string(), + path: naming::wit::package_wit_dep_dir_from_parser(&def.source_package_name) + .to_string_lossy() + .to_string(), }, ); } else { wit_dependencies.insert( format_package_name_without_version(&dep_package.name), WitDependency { - path: naming::wit::package_wit_dep_dir_from_package_dir_name(&get_file_name( - dep_package_path, - )?) + path: naming::wit::package_wit_dep_dir_from_package_dir_name( + &PathExtra::new(&dep_package_sources.dir).file_name_to_string()?, + ) .to_string_lossy() .to_string(), }, @@ -131,7 +133,8 @@ pub fn generate_cargo_toml(def: &StubDefinition) -> anyhow::Result<()> { }), }; - let mut package = cargo_toml::Package::new(def.target_crate_name(), &def.stub_crate_version); + let mut package = + cargo_toml::Package::new(def.target_crate_name(), &def.config.stub_crate_version); package.edition = Inheritable::Set(Edition::E2021); package.metadata = Some(metadata); manifest.package = Some(package); @@ -170,8 +173,18 @@ pub fn generate_cargo_toml(def: &StubDefinition) -> anyhow::Result<()> { })); let dep_golem_wasm_rpc = Dependency::Detailed(Box::new(DependencyDetail { - version: if def.wasm_rpc_override.wasm_rpc_path_override.is_none() { - if let Some(version) = def.wasm_rpc_override.wasm_rpc_version_override.as_ref() { + version: if def + .config + .wasm_rpc_override + .wasm_rpc_path_override + .is_none() + { + if let Some(version) = def + .config + .wasm_rpc_override + .wasm_rpc_version_override + .as_ref() + { Some(version.to_string()) } else { Some(WASM_RPC_VERSION.to_string()) @@ -179,7 +192,7 @@ pub fn generate_cargo_toml(def: &StubDefinition) -> anyhow::Result<()> { } else { None }, - path: def.wasm_rpc_override.wasm_rpc_path_override.clone(), + path: def.config.wasm_rpc_override.wasm_rpc_path_override.clone(), default_features: false, features: vec!["stub".to_string()], ..Default::default() @@ -196,7 +209,7 @@ pub fn generate_cargo_toml(def: &StubDefinition) -> anyhow::Result<()> { "Generating", format!( "Cargo.toml to {}", - def.target_cargo_path().to_string_lossy() + def.target_cargo_path().log_color_highlight() ), ); fs::write(def.target_cargo_path(), cargo_toml)?; @@ -242,16 +255,19 @@ pub fn add_workspace_members(path: &Path, members: &[String]) -> anyhow::Result< let cargo_toml = toml::to_string(&manifest)?; - log_action("Writing", format!("updated Cargo.toml to {:?}", path)); + log_action( + "Writing", + format!("updated Cargo.toml to {:?}", path.log_color_highlight()), + ); fs::write(path, cargo_toml)?; Ok(()) } -pub fn add_dependencies_to_cargo_toml( - cargo_path: &Path, +pub fn add_cargo_package_component_deps( + cargo_toml_path: &Path, wit_sources: BTreeMap, ) -> anyhow::Result<()> { - let raw_manifest = fs::read_to_string(cargo_path)?; + let raw_manifest = fs::read_to_string(cargo_toml_path)?; let mut manifest: Manifest = Manifest::from_slice_with_metadata(raw_manifest.as_bytes())?; if let Some(ref mut package) = manifest.package { @@ -278,8 +294,11 @@ pub fn add_dependencies_to_cargo_toml( let cargo_toml = toml::to_string(&manifest)?; - log_warn_action("Updating", format!("Cargo.toml at {:?}", cargo_path)); - fs::write(cargo_path, cargo_toml)?; + log_warn_action( + "Updating", + format!("Cargo.toml at {:?}", cargo_toml_path.log_color_highlight()), + ); + fs::write(cargo_toml_path, cargo_toml)?; } } } @@ -287,6 +306,89 @@ pub fn add_dependencies_to_cargo_toml( Ok(()) } +pub fn regenerate_cargo_package_component( + cargo_toml_path: &Path, + wit_path: &Path, + world: Option, +) -> anyhow::Result<()> { + let cargo_toml_path = PathExtra::new(cargo_toml_path); + + log_warn_action( + "Regenerating", + format!( + "package component in {}", + cargo_toml_path.log_color_highlight() + ), + ); + + let project_root = cargo_toml_path.parent()?; + let relative_wit_path = wit_path.strip_prefix(project_root).with_context(|| { + anyhow!( + "Failed to create relative path for wit dir: {}, project root: {}", + wit_path.log_color_highlight(), + project_root.log_color_highlight() + ) + })?; + + let raw_manifest = fs::read_to_string(&cargo_toml_path).with_context(|| { + anyhow!( + "Failed to read Cargo.toml at {}", + cargo_toml_path.log_color_highlight() + ) + })?; + let mut manifest: Manifest = + Manifest::from_slice_with_metadata(raw_manifest.as_bytes()).with_context(|| { + anyhow!( + "Failed to parse Cargo.toml at {}", + cargo_toml_path.log_color_highlight() + ) + })?; + let package = manifest.package.as_mut().ok_or_else(|| { + anyhow!( + "No package found in {}", + cargo_toml_path.log_color_highlight() + ) + })?; + + let wit_dir = ResolvedWitDir::new(wit_path)?; + + package.metadata = Some(MetadataRoot { + component: Some(ComponentMetadata { + package: None, + target: Some(ComponentTarget { + world, + path: relative_wit_path.to_string_lossy().to_string(), + dependencies: wit_dir + .package_sources + .iter() + .filter(|(&package_id, _)| package_id != wit_dir.package_id) + .map(|(package_id, package_sources)| { + ( + format_package_name_without_version( + &wit_dir.package(*package_id).unwrap().name, + ), + WitDependency { + path: PathExtra::new( + PathExtra::new(&package_sources.dir) + .strip_prefix(project_root) + .unwrap(), + ) + .to_string() + .unwrap(), + }, + ) + }) + .collect(), + }), + }), + }); + + let cargo_toml = toml::to_string(&manifest)?; + fs::write(cargo_toml_path, cargo_toml)?; + + Ok(()) +} + fn format_package_name_without_version(package_name: &PackageName) -> String { format!("{}:{}", package_name.namespace, package_name.name) } diff --git a/wasm-rpc-stubgen/src/commands/app.rs b/wasm-rpc-stubgen/src/commands/app.rs new file mode 100644 index 00000000..595cab53 --- /dev/null +++ b/wasm-rpc-stubgen/src/commands/app.rs @@ -0,0 +1,1318 @@ +use crate::cargo::regenerate_cargo_package_component; +use crate::fs; +use crate::fs::PathExtra; +use crate::log::{ + log_action, log_skipping_up_to_date, log_validated_action_result, log_warn_action, LogColorize, + LogIndent, +}; +use crate::model::app::{ + includes_from_yaml_file, Application, ComponentName, ComponentPropertiesExtensions, + ProfileName, DEFAULT_CONFIG_FILE_NAME, +}; +use crate::model::app_raw; +use crate::model::app_raw::ExternalCommand; +use crate::stub::{StubConfig, StubDefinition}; +use crate::validation::ValidatedResult; +use crate::wit_generate::{ + add_stub_as_dependency_to_wit_dir, extract_main_interface_as_wit_dep, AddStubAsDepConfig, + UpdateCargoToml, +}; +use crate::wit_resolve::{ResolvedWitApplication, WitDepsResolver}; +use crate::{commands, naming, WasmRpcOverride}; +use anyhow::{anyhow, bail, Context, Error}; +use colored::Colorize; +use glob::{glob_with, MatchOptions}; +use golem_wasm_rpc::WASM_RPC_VERSION; +use itertools::Itertools; +use std::cell::OnceCell; +use std::cmp::Ordering; +use std::collections::{BTreeSet, HashMap}; +use std::marker::PhantomData; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::SystemTime; +use walkdir::WalkDir; + +pub struct Config { + pub app_resolve_mode: ApplicationSourceMode, + pub skip_up_to_date_checks: bool, + pub profile: Option, + pub offline: bool, + pub extensions: PhantomData, +} + +#[derive(Debug, Clone)] +pub enum ApplicationSourceMode { + Automatic, + Explicit(Vec), +} + +pub struct ApplicationContext { + pub config: Config, + pub application: Application, + pub wit: ResolvedWitApplication, + common_wit_deps: OnceCell>, + component_generated_base_wit_deps: HashMap, +} + +impl ApplicationContext { + pub fn new(config: Config) -> anyhow::Result> { + let ctx = to_anyhow( + "Failed to create application context, see problems above", + load_app_validated(&config).and_then(|application| { + ResolvedWitApplication::new(&application, config.profile.as_ref()).map(|wit| { + ApplicationContext { + config, + application, + wit, + common_wit_deps: OnceCell::new(), + component_generated_base_wit_deps: HashMap::new(), + } + }) + }), + )?; + + // Selecting and validating profiles + { + match &ctx.config.profile { + Some(profile) => { + let all_profiles = ctx.application.all_profiles(); + if all_profiles.is_empty() { + bail!( + "Profile {} not found, no available profiles", + profile.as_str().log_color_error_highlight(), + ); + } else if !all_profiles.contains(profile) { + bail!( + "Profile {} not found, available profiles: {}", + profile.as_str().log_color_error_highlight(), + all_profiles + .into_iter() + .map(|s| s.as_str().log_color_highlight()) + .join(", ") + ); + } + log_action( + "Selecting", + format!( + "profiles, requested profile: {}", + profile.as_str().log_color_highlight() + ), + ); + } + None => { + log_action("Selecting", "profiles, no profile was requested"); + } + } + + let _indent = LogIndent::new(); + for component_name in ctx.application.component_names() { + let selection = ctx + .application + .component_effective_property_source(component_name, ctx.profile()); + + let message = match ( + selection.profile, + selection.template_name, + ctx.profile().is_some(), + selection.is_requested_profile, + ) { + (None, None, false, _) => { + format!( + "default build for {}", + component_name.as_str().log_color_highlight() + ) + } + (None, None, true, _) => { + format!( + "default build for {}, component has no profiles", + component_name.as_str().log_color_highlight() + ) + } + (None, Some(template), false, _) => { + format!( + "default build for {} using template {}{}", + component_name.as_str().log_color_highlight(), + template.as_str().log_color_highlight(), + if selection.any_template_overrides { + " with overrides" + } else { + "" + } + ) + } + (None, Some(template), true, _) => { + format!( + "default build for {} using template {}{}, component has no profiles", + component_name.as_str().log_color_highlight(), + template.as_str().log_color_highlight(), + if selection.any_template_overrides { + " with overrides" + } else { + "" + } + ) + } + (Some(profile), None, false, false) => { + format!( + "default profile {} for {}", + profile.as_str().log_color_highlight(), + component_name.as_str().log_color_highlight() + ) + } + (Some(profile), None, true, false) => { + format!( + "default profile {} for {}, component has no matching requested profile", + profile.as_str().log_color_highlight(), + component_name.as_str().log_color_highlight() + ) + } + (Some(profile), Some(template), false, false) => { + format!( + "default profile {} for {} using template {}{}", + profile.as_str().log_color_highlight(), + component_name.as_str().log_color_highlight(), + template.as_str().log_color_highlight(), + if selection.any_template_overrides { + " with overrides" + } else { + "" + } + ) + } + (Some(profile), Some(template), true, false) => { + format!( + "default profile {} for {} using template {}{}, component has no matching requested profile", + profile.as_str().log_color_highlight(), + component_name.as_str().log_color_highlight(), + template.as_str().log_color_highlight(), + if selection.any_template_overrides { + " with overrides" + } else { + "" + } + ) + } + (Some(profile), None, false, true) => { + format!( + "profile {} for {}", + profile.as_str().log_color_highlight(), + component_name.as_str().log_color_highlight() + ) + } + (Some(profile), None, true, true) => { + format!( + "requested profile {} for {}", + profile.as_str().log_color_highlight(), + component_name.as_str().log_color_highlight() + ) + } + (Some(profile), Some(template), false, true) => { + format!( + "profile {} for {} using template {}{}", + profile.as_str().log_color_highlight(), + component_name.as_str().log_color_highlight(), + template.as_str().log_color_highlight(), + if selection.any_template_overrides { + " with overrides" + } else { + "" + } + ) + } + (Some(profile), Some(template), true, true) => { + format!( + "requested profile {} for {} using template {}{}", + profile.as_str().log_color_highlight(), + component_name.as_str().log_color_highlight(), + template.as_str().log_color_highlight(), + if selection.any_template_overrides { + " with overrides" + } else { + "" + } + ) + } + }; + + log_action("Selected", message); + } + } + + if ctx.config.offline { + log_action("Selected", "offline mode"); + } + + Ok(ctx) + } + + fn profile(&self) -> Option<&ProfileName> { + self.config.profile.as_ref() + } + + fn update_wit_context(&mut self) -> anyhow::Result<()> { + to_anyhow( + "Failed to update application wit context, see problems above", + ResolvedWitApplication::new(&self.application, self.profile()).map(|wit| { + self.wit = wit; + }), + ) + } + + fn common_wit_deps(&self) -> anyhow::Result<&WitDepsResolver> { + match self + .common_wit_deps + .get_or_init(|| { + let sources = self.application.wit_deps(); + if sources.is_empty() { + bail!("No common witDeps were defined in the application manifest") + } + WitDepsResolver::new(sources) + }) + .as_ref() + { + Ok(wit_deps) => Ok(wit_deps), + Err(err) => Err(anyhow!("Failed to init wit dependency resolver: {}", err)), + } + } + + fn component_base_output_wit_deps( + &mut self, + component_name: &ComponentName, + ) -> anyhow::Result<&WitDepsResolver> { + // Not using the entry API, so we can skip copying the component name + if !self + .component_generated_base_wit_deps + .contains_key(component_name) + { + self.component_generated_base_wit_deps.insert( + component_name.clone(), + WitDepsResolver::new(vec![self + .application + .component_generated_base_wit(component_name) + .join(naming::wit::DEPS_DIR)])?, + ); + } + Ok(self + .component_generated_base_wit_deps + .get(component_name) + .unwrap()) + } +} + +pub async fn pre_component_build( + config: Config, +) -> anyhow::Result<()> { + let mut ctx = ApplicationContext::new(config)?; + pre_component_build_ctx(&mut ctx).await +} + +async fn pre_component_build_ctx( + ctx: &mut ApplicationContext, +) -> anyhow::Result<()> { + log_action("Executing", "pre-component-build steps"); + let _indent = LogIndent::new(); + + { + for component_name in ctx.wit.component_order_cloned() { + create_generated_base_wit(ctx, &component_name)?; + } + for component_name in &ctx.application.all_wasm_rpc_dependencies() { + build_stub(ctx, component_name).await?; + } + } + + { + let mut any_changed = false; + for component_name in ctx.application.component_names() { + let changed = create_generated_wit(ctx, component_name)?; + if changed { + // TODO: if this fails, it won't be retried, add done file for this + update_cargo_toml(ctx, component_name)?; + } + any_changed |= changed; + } + if any_changed { + ctx.update_wit_context()?; + } + } + + Ok(()) +} + +pub fn component_build( + config: Config, +) -> anyhow::Result<()> { + let ctx = ApplicationContext::new(config)?; + component_build_ctx(&ctx) +} + +fn component_build_ctx( + ctx: &ApplicationContext, +) -> anyhow::Result<()> { + log_action("Executing", "component-build steps"); + let _indent = LogIndent::new(); + + log_action("Building", "components"); + let _indent = LogIndent::new(); + + for component_name in ctx.application.component_names() { + let component_properties = ctx + .application + .component_properties(component_name, ctx.profile()); + + if component_properties.build.is_empty() { + log_warn_action( + "Skipping", + format!( + "building {}, no build steps", + component_name.as_str().log_color_highlight(), + ), + ); + continue; + } + + log_action( + "Building", + format!("{}", component_name.as_str().log_color_highlight()), + ); + let _indent = LogIndent::new(); + + for build_step in &component_properties.build { + execute_external_command(ctx, component_name, build_step)?; + } + } + + Ok(()) +} + +pub async fn post_component_build( + config: Config, +) -> anyhow::Result<()> { + let ctx = ApplicationContext::new(config)?; + post_component_build_ctx(&ctx).await +} + +async fn post_component_build_ctx( + ctx: &ApplicationContext, +) -> anyhow::Result<()> { + log_action("Executing", "post-component-build steps"); + let _indent = LogIndent::new(); + + for component_name in ctx.application.component_names() { + let source = ctx.application.component_source_dir(component_name); + let dependencies = ctx + .application + .component_wasm_rpc_dependencies(component_name); + let component_wasm = ctx + .application + .component_wasm(component_name, ctx.profile()); + let linked_wasm = ctx + .application + .component_linked_wasm(component_name, ctx.profile()); + + if is_up_to_date( + ctx.config.skip_up_to_date_checks, + // We also include the component specification source, + // so it triggers build in case deps are changed + || [source.to_path_buf(), component_wasm.clone()], + || [linked_wasm.clone()], + ) { + log_skipping_up_to_date(format!( + "composing wasm rpc dependencies ({}) into {}", + dependencies + .iter() + .map(|s| s.as_str().log_color_highlight()) + .join(", "), + component_name.as_str().log_color_highlight(), + )); + continue; + } + + if dependencies.is_empty() { + log_action( + "Copying", + format!( + "(without composing) {} to {}, no wasm rpc dependencies defined", + component_wasm.log_color_highlight(), + linked_wasm.log_color_highlight(), + ), + ); + fs::copy(&component_wasm, &linked_wasm)?; + } else { + log_action( + "Composing", + format!( + "wasm rpc dependencies ({}) into {}", + dependencies + .iter() + .map(|s| s.as_str().log_color_highlight()) + .join(", "), + component_name.as_str().log_color_highlight(), + ), + ); + let _indent = LogIndent::new(); + + let stub_wasms = dependencies + .iter() + .map(|dep| ctx.application.stub_wasm(dep)) + .collect::>(); + + commands::composition::compose( + ctx.application + .component_wasm(component_name, ctx.profile()) + .as_path(), + &stub_wasms, + ctx.application + .component_linked_wasm(component_name, ctx.profile()) + .as_path(), + ) + .await?; + } + } + + Ok(()) +} + +pub async fn build(config: Config) -> anyhow::Result<()> { + let mut ctx = ApplicationContext::::new(config)?; + + pre_component_build_ctx(&mut ctx).await?; + component_build_ctx(&ctx)?; + post_component_build_ctx(&ctx).await?; + + Ok(()) +} + +pub fn clean(config: Config) -> anyhow::Result<()> { + let app = to_anyhow( + "Failed to load application manifest(s), see problems above", + load_app_validated(&config), + )?; + + { + log_action("Cleaning", "components"); + let _indent = LogIndent::new(); + + let all_profiles = app.all_option_profiles(); + let paths = { + let mut paths = BTreeSet::<(&'static str, PathBuf)>::new(); + for component_name in app.component_names() { + for profile in &all_profiles { + paths.insert(( + "generated wit", + app.component_generated_wit(component_name, profile.as_ref()), + )); + paths.insert(( + "component wasm", + app.component_wasm(component_name, profile.as_ref()), + )); + paths.insert(( + "linked wasm", + app.component_linked_wasm(component_name, profile.as_ref()), + )); + + let properties = &app.component_properties(component_name, profile.as_ref()); + + for build_step in &properties.build { + let build_dir = build_step + .dir + .as_ref() + .map(|dir| app.component_source_dir(component_name).join(dir)) + .unwrap_or_else(|| { + app.component_source_dir(component_name).to_path_buf() + }); + + paths.extend( + compile_and_collect_globs(&build_dir, &build_step.targets)? + .into_iter() + .map(|path| ("build output", path)), + ); + } + + paths.extend(properties.clean.iter().map(|path| { + ( + "clean target", + app.component_source_dir(component_name).join(path), + ) + })); + } + } + paths + }; + + for (context, path) in paths { + delete_path(context, &path)?; + } + } + + { + log_action("Cleaning", "component stubs"); + let _indent = LogIndent::new(); + + for component_name in app.all_wasm_rpc_dependencies() { + log_action( + "Cleaning", + format!( + "component stub {}", + component_name.as_str().log_color_highlight() + ), + ); + let _indent = LogIndent::new(); + + delete_path("stub wit", &app.stub_wit(&component_name))?; + delete_path("stub wasm", &app.stub_wasm(&component_name))?; + } + } + + { + log_action("Cleaning", "application build dir"); + let _indent = LogIndent::new(); + + delete_path("temp dir", app.temp_dir())?; + } + + Ok(()) +} + +pub fn available_custom_commands( + config: Config, +) -> anyhow::Result> { + let app = to_anyhow( + "Failed to load application manifest(s), see problems above", + load_app_validated(&config), + )?; + + let all_profiles = app.all_option_profiles(); + + let mut commands = BTreeSet::::new(); + for profile in &all_profiles { + commands.extend(app.all_custom_commands(profile.as_ref())) + } + + Ok(commands) +} + +pub fn custom_command( + config: Config, + command: String, +) -> anyhow::Result<()> { + let ctx = ApplicationContext::new(config)?; + + let all_custom_commands = ctx.application.all_custom_commands(ctx.profile()); + if !all_custom_commands.contains(&command) { + if all_custom_commands.is_empty() { + bail!( + "Custom command {} not found, no custom command is available", + command.log_color_error_highlight(), + ); + } else { + bail!( + "Custom command {} not found, available custom commands: {}", + command.log_color_error_highlight(), + all_custom_commands + .iter() + .map(|s| s.log_color_highlight()) + .join(", ") + ); + } + } + + log_action( + "Executing", + format!("custom command {}", command.log_color_highlight()), + ); + let _indent = LogIndent::new(); + + for component_name in ctx.application.component_names() { + let properties = &ctx + .application + .component_properties(component_name, ctx.profile()); + if let Some(custom_command) = properties.custom_commands.get(&command) { + log_action( + "Executing", + format!( + "custom command {} for component {}", + command.log_color_highlight(), + component_name.as_str().log_color_highlight() + ), + ); + let _indent = LogIndent::new(); + + for step in custom_command { + execute_external_command(&ctx, component_name, step)?; + } + } + } + + Ok(()) +} + +fn delete_path(context: &str, path: &Path) -> anyhow::Result<()> { + if path.exists() { + log_warn_action( + "Deleting", + format!("{} {}", context, path.log_color_highlight()), + ); + fs::remove(path).with_context(|| { + anyhow!( + "Failed to delete {}, path: {}", + context.log_color_highlight(), + path.log_color_highlight() + ) + })?; + } + Ok(()) +} + +fn load_app_validated( + config: &Config, +) -> ValidatedResult> { + let sources = collect_sources(&config.app_resolve_mode); + let oam_apps = sources.and_then(|sources| { + sources + .into_iter() + .map(|source| { + ValidatedResult::from_result(app_raw::ApplicationWithSource::from_yaml_file(source)) + }) + .collect::>>() + }); + + log_action("Collecting", "components"); + let _indent = LogIndent::new(); + + let app = oam_apps.and_then(Application::from_raw_apps); + + log_validated_action_result("Found", &app, |app| { + if app.component_names().next().is_none() { + "no components".to_string() + } else { + format!( + "components: {}", + app.component_names() + .map(|s| s.as_str().log_color_highlight()) + .join(", ") + ) + } + }); + + app +} + +fn collect_sources(mode: &ApplicationSourceMode) -> ValidatedResult> { + log_action("Collecting", "sources"); + let _indent = LogIndent::new(); + + let sources = match mode { + ApplicationSourceMode::Automatic => match find_main_source() { + Some(source) => { + // TODO: save original current dir and use it as a component filter + let source_ext = PathExtra::new(&source); + let source_dir = source_ext.parent().unwrap(); + std::env::set_current_dir(source_dir) + .expect("Failed to set current dir for config parent"); + + let includes = includes_from_yaml_file(source.as_path()); + if includes.is_empty() { + ValidatedResult::Ok(vec![source]) + } else { + ValidatedResult::from_result(compile_and_collect_globs(source_dir, &includes)) + .map(|mut sources| { + sources.insert(0, source); + sources + }) + } + } + None => ValidatedResult::from_error("No config file found!".to_string()), + }, + ApplicationSourceMode::Explicit(sources) => { + let non_unique_source_warns: Vec<_> = sources + .iter() + .counts() + .into_iter() + .filter(|(_, count)| *count > 1) + .map(|(source, count)| { + format!( + "Source added multiple times, source: {}, count: {}", + source.display(), + count + ) + }) + .collect(); + + ValidatedResult::from_value_and_warns(sources.clone(), non_unique_source_warns) + } + }; + + log_validated_action_result("Found", &sources, |sources| { + if sources.is_empty() { + "no sources".to_string() + } else { + format!( + "sources: {}", + sources + .iter() + .map(|source| source.log_color_highlight()) + .join(", ") + ) + } + }); + + sources +} + +fn find_main_source() -> Option { + let mut current_dir = std::env::current_dir().expect("Failed to get current dir"); + let mut last_source: Option = None; + + loop { + let file = current_dir.join(DEFAULT_CONFIG_FILE_NAME); + if current_dir.join(DEFAULT_CONFIG_FILE_NAME).exists() { + last_source = Some(file); + } + match current_dir.parent() { + Some(parent_dir) => current_dir = parent_dir.to_path_buf(), + None => { + break; + } + } + } + + last_source +} + +fn to_anyhow(message: &str, result: ValidatedResult) -> anyhow::Result { + fn print_warns(warns: Vec) { + let label = "Warning".yellow(); + for warn in warns { + eprintln!("{}: {}", label, warn); + } + } + + fn print_errors(errors: Vec) { + let label = "Error".red(); + for error in errors { + eprintln!("{}: {}", label, error); + } + } + + match result { + ValidatedResult::Ok(value) => Ok(value), + ValidatedResult::OkWithWarns(components, warns) => { + println!(); + print_warns(warns); + println!(); + Ok(components) + } + ValidatedResult::WarnsAndErrors(warns, errors) => { + println!(); + print_warns(warns); + print_errors(errors); + println!(); + + Err(anyhow!(message.to_string())) + } + } +} + +fn is_up_to_date(skip_check: bool, sources: FS, targets: FT) -> bool +where + S: IntoIterator, + T: IntoIterator, + FS: FnOnce() -> S, + FT: FnOnce() -> T, +{ + if skip_check { + return false; + } + + fn max_modified(path: &Path) -> Option { + let mut max_modified: Option = None; + let mut update_max_modified = |modified: SystemTime| { + if max_modified.map_or(true, |max_mod| max_mod.cmp(&modified) == Ordering::Less) { + max_modified = Some(modified) + } + }; + + if let Ok(metadata) = fs::metadata(path) { + if metadata.is_dir() { + WalkDir::new(path) + .into_iter() + .filter_map(|entry| entry.ok().and_then(|entry| entry.metadata().ok())) + .filter(|metadata| !metadata.is_dir()) + .filter_map(|metadata| metadata.modified().ok()) + .for_each(update_max_modified) + } else if let Ok(modified) = metadata.modified() { + update_max_modified(modified) + } + } + + max_modified + } + + fn max_modified_short_circuit_on_missing>( + paths: I, + ) -> Option { + // Using Result and collect for short-circuit on any missing mod time + paths + .into_iter() + .map(|path| max_modified(path.as_path()).ok_or(())) + .collect::, _>>() + .and_then(|mod_times| mod_times.into_iter().max().ok_or(())) + .ok() + } + + let targets = targets(); + + let max_target_modified = max_modified_short_circuit_on_missing(targets); + + let max_target_modified = match max_target_modified { + Some(modified) => modified, + None => return false, + }; + + let sources = sources(); + + let max_source_modified = max_modified_short_circuit_on_missing(sources); + + match max_source_modified { + Some(max_source_modified) => { + max_source_modified.cmp(&max_target_modified) == Ordering::Less + } + None => false, + } +} + +fn compile_and_collect_globs(root_dir: &Path, globs: &[String]) -> Result, Error> { + globs + .iter() + .map(|pattern| { + glob_with( + &format!("{}/{}", root_dir.to_string_lossy(), pattern), + MatchOptions { + case_sensitive: true, + require_literal_separator: false, + require_literal_leading_dot: true, + }, + ) + .with_context(|| format!("Failed to compile glob expression: {}", pattern)) + }) + .collect::, _>>() + .map_err(|err| anyhow!(err)) + .and_then(|paths| { + paths + .into_iter() + .flatten() + .collect::, _>>() + .map_err(|err| anyhow!(err)) + }) +} + +fn create_generated_base_wit( + ctx: &mut ApplicationContext, + component_name: &ComponentName, +) -> Result { + let component_source_wit = ctx + .application + .component_source_wit(component_name, ctx.profile()); + let component_generated_base_wit = ctx.application.component_generated_base_wit(component_name); + let gen_dir_done_marker = GeneratedDirDoneMarker::new(&component_generated_base_wit); + + if is_up_to_date( + ctx.config.skip_up_to_date_checks + || !gen_dir_done_marker.is_done() + || !ctx.wit.is_dep_graph_up_to_date(component_name)?, + || [component_source_wit.clone()], + || [component_generated_base_wit.clone()], + ) { + log_skipping_up_to_date(format!( + "creating generated base wit directory for {}", + component_name.as_str().log_color_highlight() + )); + Ok(false) + } else { + log_action( + "Creating", + format!( + "generated base wit directory for {}", + component_name.as_str().log_color_highlight(), + ), + ); + let _indent = LogIndent::new(); + + delete_path( + "generated base wit directory", + &component_generated_base_wit, + )?; + copy_wit_sources(&component_source_wit, &component_generated_base_wit)?; + + { + let missing_package_deps = ctx + .wit + .missing_generic_source_package_deps(component_name)?; + + if !missing_package_deps.is_empty() { + log_action("Adding", "package deps"); + let _indent = LogIndent::new(); + + ctx.common_wit_deps()? + .add_packages_with_transitive_deps_to_wit_dir( + &missing_package_deps, + &component_generated_base_wit, + )?; + } + } + + { + let component_interface_package_deps = + ctx.wit.component_interface_package_deps(component_name)?; + if !component_interface_package_deps.is_empty() { + log_action("Adding", "component interface package dependencies"); + let _indent = LogIndent::new(); + + for (dep_interface_package_name, dep_component_name) in + &component_interface_package_deps + { + ctx.component_base_output_wit_deps(dep_component_name)? + .add_packages_with_transitive_deps_to_wit_dir( + &[dep_interface_package_name.clone()], + &component_generated_base_wit, + )?; + } + } + } + + { + log_action( + "Extracting", + format!( + "main interface package from {} to {}", + component_source_wit.log_color_highlight(), + component_generated_base_wit.log_color_highlight() + ), + ); + let _indent = LogIndent::new(); + + extract_main_interface_as_wit_dep(&component_generated_base_wit)?; + } + + gen_dir_done_marker.mark_as_done()?; + + Ok(true) + } +} + +fn create_generated_wit( + ctx: &ApplicationContext, + component_name: &ComponentName, +) -> Result { + let component_generated_base_wit = ctx.application.component_generated_base_wit(component_name); + let component_generated_wit = ctx + .application + .component_generated_wit(component_name, ctx.profile()); + let gen_dir_done_marker = GeneratedDirDoneMarker::new(&component_generated_wit); + + if is_up_to_date( + ctx.config.skip_up_to_date_checks + || !gen_dir_done_marker.is_done() + || !ctx.wit.is_dep_graph_up_to_date(component_name)?, + || [component_generated_base_wit.clone()], + || [component_generated_wit.clone()], + ) { + log_skipping_up_to_date(format!( + "creating generated wit directory for {}", + component_name.as_str().log_color_highlight() + )); + Ok(false) + } else { + log_action( + "Creating", + format!( + "generated wit directory for {}", + component_name.as_str().log_color_highlight(), + ), + ); + let _indent = LogIndent::new(); + + delete_path("generated wit directory", &component_generated_wit)?; + copy_wit_sources(&component_generated_base_wit, &component_generated_wit)?; + add_stub_deps(ctx, component_name)?; + + gen_dir_done_marker.mark_as_done()?; + + Ok(true) + } +} + +fn update_cargo_toml( + ctx: &ApplicationContext, + component_name: &ComponentName, +) -> anyhow::Result<()> { + let component_source_wit = PathExtra::new( + ctx.application + .component_source_wit(component_name, ctx.profile()), + ); + let component_source_wit_parent = component_source_wit.parent().with_context(|| { + anyhow!( + "Failed to get parent for component {}", + component_name.as_str().log_color_highlight() + ) + })?; + let cargo_toml = component_source_wit_parent.join("Cargo.toml"); + + if cargo_toml.exists() { + regenerate_cargo_package_component( + &cargo_toml, + &ctx.application + .component_generated_wit(component_name, ctx.profile()), + None, + )? + } + + Ok(()) +} + +async fn build_stub( + ctx: &ApplicationContext, + component_name: &ComponentName, +) -> anyhow::Result { + let target_root = ctx.application.stub_temp_build_dir(component_name); + + let stub_def = StubDefinition::new(StubConfig { + source_wit_root: ctx.application.component_generated_base_wit(component_name), + target_root: target_root.clone(), + selected_world: None, + stub_crate_version: WASM_RPC_VERSION.to_string(), + wasm_rpc_override: WasmRpcOverride { + wasm_rpc_path_override: None, + wasm_rpc_version_override: None, + }, + extract_source_interface_package: false, + seal_cargo_workspace: true, + }) + .context("Failed to gather information for the stub generator")?; + + let stub_dep_package_ids = stub_def.stub_dep_package_ids(); + let stub_sources: Vec = stub_def + .packages_with_wit_sources() + .flat_map(|(package_id, _, sources)| { + (stub_dep_package_ids.contains(&package_id) || package_id == stub_def.source_package_id) + .then(|| sources.files.iter().cloned()) + .unwrap_or_default() + }) + .collect(); + + let stub_wasm = ctx.application.stub_wasm(component_name); + let stub_wit = ctx.application.stub_wit(component_name); + let gen_dir_done_marker = GeneratedDirDoneMarker::new(&stub_wit); + + if is_up_to_date( + ctx.config.skip_up_to_date_checks || !gen_dir_done_marker.is_done(), + || stub_sources, + || [stub_wit.clone(), stub_wasm.clone()], + ) { + log_skipping_up_to_date(format!( + "building wasm rpc stub for {}", + component_name.as_str().log_color_highlight() + )); + Ok(false) + } else { + log_action( + "Building", + format!( + "wasm rpc stub for {}", + component_name.as_str().log_color_highlight() + ), + ); + let _indent = LogIndent::new(); + + delete_path("stub temp build dir", &target_root)?; + delete_path("stub wit", &stub_wit)?; + delete_path("stub wasm", &stub_wasm)?; + + log_action( + "Creating", + format!("stub temp build dir {}", target_root.log_color_highlight()), + ); + fs::create_dir_all(&target_root)?; + + commands::generate::build(&stub_def, &stub_wasm, &stub_wit, ctx.config.offline).await?; + + gen_dir_done_marker.mark_as_done()?; + + delete_path("stub temp build dir", &target_root)?; + + Ok(true) + } +} + +fn add_stub_deps( + ctx: &ApplicationContext, + component_name: &ComponentName, +) -> Result { + let dependencies = ctx + .application + .component_wasm_rpc_dependencies(component_name); + if dependencies.is_empty() { + Ok(false) + } else { + log_action( + "Adding", + format!( + "stub wit dependencies to {}", + component_name.as_str().log_color_highlight() + ), + ); + + let _indent = LogIndent::new(); + + for dep_component_name in dependencies { + log_action( + "Adding", + format!( + "{} stub wit dependency to {}", + dep_component_name.as_str().log_color_highlight(), + component_name.as_str().log_color_highlight() + ), + ); + let _indent = LogIndent::new(); + + add_stub_as_dependency_to_wit_dir(AddStubAsDepConfig { + stub_wit_root: ctx.application.stub_wit(dep_component_name), + dest_wit_root: ctx + .application + .component_generated_wit(component_name, ctx.profile()), + update_cargo_toml: UpdateCargoToml::NoUpdate, + })? + } + + Ok(true) + } +} + +fn copy_wit_sources(source: &Path, target: &Path) -> anyhow::Result<()> { + log_action( + "Copying", + format!( + "wit sources from {} to {}", + source.log_color_highlight(), + target.log_color_highlight() + ), + ); + let _indent = LogIndent::new(); + + let dir_content = fs_extra::dir::get_dir_content(source).with_context(|| { + anyhow!( + "Failed to read component source wit directory entries for {}", + source.log_color_highlight() + ) + })?; + + for file in dir_content.files { + let from = PathBuf::from(&file); + let to = target.join(from.strip_prefix(source).with_context(|| { + anyhow!( + "Failed to strip prefix for source {}", + &file.log_color_highlight() + ) + })?); + + log_action( + "Copying", + format!( + "wit source {} to {}", + from.log_color_highlight(), + to.log_color_highlight() + ), + ); + fs::copy(from, to)?; + } + + Ok(()) +} + +fn execute_external_command( + ctx: &ApplicationContext, + component_name: &ComponentName, + command: &ExternalCommand, +) -> anyhow::Result<()> { + let build_dir = command + .dir + .as_ref() + .map(|dir| { + ctx.application + .component_source_dir(component_name) + .join(dir) + }) + .unwrap_or_else(|| { + ctx.application + .component_source_dir(component_name) + .to_path_buf() + }); + + if !command.sources.is_empty() && !command.targets.is_empty() { + let sources = compile_and_collect_globs(&build_dir, &command.sources)?; + let targets = compile_and_collect_globs(&build_dir, &command.targets)?; + + if is_up_to_date(ctx.config.skip_up_to_date_checks, || sources, || targets) { + log_skipping_up_to_date(format!( + "executing external command '{}' in directory {}", + command.command.log_color_highlight(), + build_dir.log_color_highlight() + )); + return Ok(()); + } + } + + log_action( + "Executing", + format!( + "external command '{}' in directory {}", + command.command.log_color_highlight(), + build_dir.log_color_highlight() + ), + ); + + let command_tokens = command.command.split(' ').collect::>(); + if command_tokens.is_empty() { + return Err(anyhow!("Empty command!")); + } + + let result = Command::new(command_tokens[0]) + .args(command_tokens.iter().skip(1)) + .current_dir(build_dir) + .status() + .with_context(|| "Failed to execute command".to_string())?; + + if !result.success() { + return Err(anyhow!(format!( + "Command failed with exit code: {}", + result + .code() + .map(|code| code.to_string().log_color_error_highlight().to_string()) + .unwrap_or_else(|| "?".to_string()) + ))); + } + + Ok(()) +} + +static GENERATED_DIR_DONE_MARKER_FILE_NAME: &str = ".done"; + +struct GeneratedDirDoneMarker<'a> { + dir: &'a Path, +} + +impl<'a> GeneratedDirDoneMarker<'a> { + fn new(dir: &'a Path) -> Self { + Self { dir } + } + + fn is_done(&self) -> bool { + self.dir.join(GENERATED_DIR_DONE_MARKER_FILE_NAME).exists() + } + + fn mark_as_done(&self) -> anyhow::Result<()> { + fs::write_str(self.dir.join(GENERATED_DIR_DONE_MARKER_FILE_NAME), "") + } +} diff --git a/wasm-rpc-stubgen/src/commands/composition.rs b/wasm-rpc-stubgen/src/commands/composition.rs index 4f049506..4746b64a 100644 --- a/wasm-rpc-stubgen/src/commands/composition.rs +++ b/wasm-rpc-stubgen/src/commands/composition.rs @@ -1,4 +1,6 @@ -use crate::commands::log::log_warn_action; +use crate::fs; +use crate::fs::PathExtra; +use crate::log::{log_warn_action, LogColorize}; use anyhow::Context; use std::collections::{BTreeMap, BTreeSet}; use std::path::{Path, PathBuf}; @@ -14,14 +16,11 @@ pub async fn compose( // with allowing missing plugs (through the also customized plug function below) // and using local packages only (for now) + let dest_wasm = PathExtra::new(dest_wasm); + let mut graph = CompositionGraph::new(); - let socket = std::fs::read(source_wasm).with_context(|| { - format!( - "failed to read socket component `{socket}`", - socket = source_wasm.to_string_lossy() - ) - })?; + let socket = fs::read(source_wasm).context("Failed to read socket component")?; let socket = Package::from_bytes("socket", None, socket, graph.types_mut())?; let socket = graph.register_package(socket)?; @@ -42,10 +41,8 @@ pub async fn compose( let bytes = graph.encode(EncodeOptions::default())?; - std::fs::write(dest_wasm, bytes).context(format!( - "failed to write output file `{path}`", - path = dest_wasm.display() - ))?; + fs::create_dir_all(dest_wasm.parent()?)?; + fs::write(dest_wasm, bytes)?; Ok(()) } @@ -108,7 +105,10 @@ fn plug( }; for plug_name in unused_plugs { - log_warn_action("Skipping", format!("{}, not used", plug_name)); + log_warn_action( + "Skipping", + format!("{}, not used", plug_name.log_color_highlight()), + ); } // Export all exports from the socket component. diff --git a/wasm-rpc-stubgen/src/commands/declarative.rs b/wasm-rpc-stubgen/src/commands/declarative.rs deleted file mode 100644 index 12bef4b1..00000000 --- a/wasm-rpc-stubgen/src/commands/declarative.rs +++ /dev/null @@ -1,534 +0,0 @@ -use crate::commands::dependencies::UpdateCargoToml; -use crate::commands::log::{ - log_action, log_skipping_up_to_date, log_validated_action_result, log_warn_action, -}; -use crate::fs::copy; -use crate::model::oam; -use crate::model::validation::ValidatedResult; -use crate::model::wasm_rpc::{ - include_glob_patter_from_yaml_file, init_oam_app, Application, DEFAULT_CONFIG_FILE_NAME, -}; -use crate::stub::StubDefinition; -use crate::{commands, WasmRpcOverride}; -use anyhow::{anyhow, Context, Error}; -use colored::Colorize; -use glob::glob; -use itertools::Itertools; -use std::cmp::Ordering; -use std::io::Write; -use std::path::{Path, PathBuf}; -use std::process::Command; -use std::time::SystemTime; -use tempfile::TempDir; -use walkdir::WalkDir; - -pub struct Config { - pub app_resolve_mode: ApplicationResolveMode, - pub skip_up_to_date_checks: bool, -} - -#[derive(Debug, Clone)] -pub enum ApplicationResolveMode { - Automatic, - Explicit(Vec), -} - -pub fn init(component_name: String) -> anyhow::Result<()> { - let file_name = DEFAULT_CONFIG_FILE_NAME; - - let mut file = std::fs::File::create_new(file_name) - .with_context(|| format!("Failed to create {}", file_name))?; - - let app = init_oam_app(component_name); - file.write_all(app.to_yaml_string().as_bytes()) - .with_context(|| format!("Failed to write {}", file_name))?; - - Ok(()) -} - -pub async fn pre_component_build(config: Config) -> anyhow::Result<()> { - let app = load_app(&config)?; - pre_component_build_app(&config, &app).await -} - -async fn pre_component_build_app(config: &Config, app: &Application) -> anyhow::Result<()> { - if app.all_wasm_rpc_dependencies().is_empty() { - log_warn_action("Skipping", "building wasm rpc stubs, no dependency found"); - } else { - log_action("Building", "wasm rpc stubs"); - for component_name in app.all_wasm_rpc_dependencies() { - if is_up_to_date( - config.skip_up_to_date_checks, - || [app.component_wit(&component_name)], - || { - [ - app.stub_wasm(&component_name), - app.stub_wit(&component_name), - ] - }, - ) { - log_skipping_up_to_date(format!("building wasm rpc stub: {}", component_name)); - continue; - } - - log_action("Building", format!("wasm rpc stub: {}", component_name)); - - let target_root = TempDir::new()?; - let canonical_target_root = target_root.path().canonicalize()?; - - let stub_def = StubDefinition::new( - &app.component_wit(&component_name), - &canonical_target_root, - &app.stub_world(&component_name), - &app.stub_crate_version(&component_name), - &WasmRpcOverride { - wasm_rpc_path_override: app.stub_wasm_rpc_path(&component_name), - wasm_rpc_version_override: app.stub_wasm_rpc_version(&component_name), - }, - app.stub_always_inline_types(&component_name), - ) - .context("Failed to gather information for the stub generator")?; - - commands::generate::build( - &stub_def, - &app.stub_wasm(&component_name), - &app.stub_wit(&component_name), - ) - .await? - } - } - - for (component_name, component) in &app.wasm_components_by_name { - if !component.wasm_rpc_dependencies.is_empty() { - log_action( - "Adding", - format!("stub wit dependencies to {}", component_name), - ) - } - - for dep_component_name in &component.wasm_rpc_dependencies { - // TODO: this should check into the wit deps for the specific stubs or do folder diffs - if is_up_to_date( - config.skip_up_to_date_checks, - || [app.stub_wit(dep_component_name)], - || [app.component_wit(component_name)], - ) { - log_skipping_up_to_date(format!( - "adding {} stub wit dependency to {}", - dep_component_name, component_name - )); - continue; - } - - log_action( - "Adding", - format!( - "{} stub wit dependency to {}", - dep_component_name, component_name - ), - ); - - commands::dependencies::add_stub_dependency( - &app.stub_wit(dep_component_name), - &app.component_wit(component_name), - true, // NOTE: in declarative mode we always use overwrite - UpdateCargoToml::UpdateIfExists, - )? - } - } - - Ok(()) -} - -pub fn component_build(config: Config) -> anyhow::Result<()> { - let app = load_app(&config)?; - component_build_app(&config, &app) -} - -pub fn component_build_app(config: &Config, app: &Application) -> anyhow::Result<()> { - let components_with_build_steps = app - .wasm_components_by_name - .values() - .filter(|component| !component.build_steps.is_empty()) - .collect::>(); - - if components_with_build_steps.is_empty() { - log_warn_action( - "Skipping", - "building components, no components with build steps found", - ); - return Ok(()); - } - - log_action("Building", "components"); - - for component in components_with_build_steps { - log_action("Building", format!("component: {}", component.name)); - for build_step in &component.build_steps { - let build_dir = build_step - .dir - .as_ref() - .map(|dir| component.source_dir().join(dir)) - .unwrap_or_else(|| component.source_dir().to_path_buf()); - - if !build_step.inputs.is_empty() && !build_step.outputs.is_empty() { - let inputs = compile_and_collect_globs(&build_dir, &build_step.inputs)?; - let outputs = compile_and_collect_globs(&build_dir, &build_step.outputs)?; - - if is_up_to_date(config.skip_up_to_date_checks, || inputs, || outputs) { - log_skipping_up_to_date(format!("executing command: {}", build_step.command)); - continue; - } - } - - log_action("Executing", format!("command: {}", build_step.command)); - - let command_tokens = build_step.command.split(' ').collect::>(); - if command_tokens.is_empty() { - return Err(anyhow!("Empty command!")); - } - - let result = Command::new(command_tokens[0]) - .args(command_tokens.iter().skip(1)) - .current_dir(build_dir) - .status() - .with_context(|| "Failed to execute command".to_string())?; - - if !result.success() { - return Err(anyhow!(format!( - "Command failed with exit code: {}", - result - .code() - .map(|code| code.to_string()) - .unwrap_or_else(|| "?".to_string()) - ))); - } - } - } - - Ok(()) -} - -pub async fn post_component_build(config: Config) -> anyhow::Result<()> { - let app = load_app(&config)?; - post_component_build_app(&config, &app).await -} - -pub async fn post_component_build_app(config: &Config, app: &Application) -> anyhow::Result<()> { - for (component_name, component) in &app.wasm_components_by_name { - let input_wasm = app.component_input_wasm(component_name); - let output_wasm = app.component_output_wasm(component_name); - - if is_up_to_date( - config.skip_up_to_date_checks, - // We also include the component specification source, - // so it triggers build in case deps are changed - || [component.source.clone(), input_wasm.clone()], - || [output_wasm.clone()], - ) { - log_skipping_up_to_date(format!( - "composing wasm rpc dependencies ({}) into {}", - component.wasm_rpc_dependencies.iter().join(", "), - component_name, - )); - continue; - } - - if component.wasm_rpc_dependencies.is_empty() { - log_action( - "Copying", - format!( - "(without composing) {} to {}, no wasm rpc dependencies defined", - input_wasm.to_string_lossy(), - output_wasm.to_string_lossy() - ), - ); - copy(&input_wasm, &output_wasm)?; - } else { - log_action( - "Composing", - format!( - "wasm rpc dependencies ({}) into {}", - component.wasm_rpc_dependencies.iter().join(", "), - component_name, - ), - ); - - let stub_wasms = component - .wasm_rpc_dependencies - .iter() - .map(|dep| app.stub_wasm(dep)) - .collect::>(); - - commands::composition::compose( - app.component_input_wasm(component_name).as_path(), - &stub_wasms, - app.component_output_wasm(component_name).as_path(), - ) - .await?; - } - } - - Ok(()) -} - -pub async fn build(config: Config) -> anyhow::Result<()> { - let app = load_app(&config)?; - - pre_component_build_app(&config, &app).await?; - component_build_app(&config, &app)?; - post_component_build_app(&config, &app).await?; - - Ok(()) -} - -fn load_app(config: &Config) -> anyhow::Result { - to_anyhow( - "Failed to load application manifest(s), see problems above".to_string(), - load_app_validated(config), - ) -} - -fn load_app_validated(config: &Config) -> ValidatedResult { - let sources = collect_sources(&config.app_resolve_mode); - let oam_apps = sources.and_then(|sources| { - sources - .into_iter() - .map(|source| { - ValidatedResult::from_result(oam::ApplicationWithSource::from_yaml_file(source)) - .and_then(oam::ApplicationWithSource::validate) - }) - .collect::>>() - }); - - log_action("Collecting", "components"); - - let app = oam_apps.and_then(Application::from_oam_apps); - - log_validated_action_result("Found", &app, |app| { - if app.wasm_components_by_name.is_empty() { - "no components".to_string() - } else { - format!( - "components: {}", - app.wasm_components_by_name.keys().join(", ") - ) - } - }); - - app -} - -fn collect_sources(mode: &ApplicationResolveMode) -> ValidatedResult> { - log_action("Collecting", "sources"); - - let sources = match mode { - ApplicationResolveMode::Automatic => match find_main_source() { - Some(source) => { - std::env::set_current_dir(source.parent().expect("Failed ot get config parent")) - .expect("Failed to set current dir for config parent"); - - match include_glob_patter_from_yaml_file(source.as_path()) { - Some(pattern) => ValidatedResult::from_result( - glob(pattern.as_str()) - .map_err(|err| { - format!( - "Failed to compile glob pattern: {}, source: {}, error: {}", - pattern, - source.to_string_lossy(), - err - ) - }) - .and_then(|matches| { - matches.collect::, _>>().map_err(|err| { - format!( - "Failed to resolve glob pattern: {}, source: {}, error: {}", - pattern, - source.to_string_lossy(), - err - ) - }) - }), - ) - .map(|mut sources| { - sources.insert(0, source); - sources - }), - None => ValidatedResult::Ok(vec![source]), - } - } - None => ValidatedResult::from_error("No config file found!".to_string()), - }, - ApplicationResolveMode::Explicit(sources) => { - let non_unique_source_warns: Vec<_> = sources - .iter() - .counts() - .into_iter() - .filter(|(_, count)| *count > 1) - .map(|(source, count)| { - format!( - "Source added multiple times, source: {}, count: {}", - source.to_string_lossy(), - count - ) - }) - .collect(); - - ValidatedResult::from_value_and_warns(sources.clone(), non_unique_source_warns) - } - }; - - log_validated_action_result("Found", &sources, |sources| { - if sources.is_empty() { - "no sources".to_string() - } else { - format!( - "sources: {}", - sources - .iter() - .map(|source| source.to_string_lossy()) - .join(", ") - ) - } - }); - - sources -} - -fn find_main_source() -> Option { - let mut current_dir = std::env::current_dir().expect("Failed to get current dir"); - let mut last_source: Option = None; - - loop { - let file = current_dir.join(DEFAULT_CONFIG_FILE_NAME); - if current_dir.join(DEFAULT_CONFIG_FILE_NAME).exists() { - last_source = Some(file); - } - match current_dir.parent() { - Some(parent_dir) => current_dir = parent_dir.to_path_buf(), - None => { - break; - } - } - } - - last_source -} - -fn to_anyhow(message: String, result: ValidatedResult) -> anyhow::Result { - fn print_warns(warns: Vec) { - let label = "Warning".yellow(); - for warn in warns { - eprintln!("{}: {}", label, warn); - } - } - - fn print_errors(errors: Vec) { - let label = "Error".red(); - for error in errors { - eprintln!("{}: {}", label, error); - } - } - - match result { - ValidatedResult::Ok(value) => Ok(value), - ValidatedResult::OkWithWarns(components, warns) => { - print_warns(warns); - println!(); - Ok(components) - } - ValidatedResult::WarnsAndErrors(warns, errors) => { - print_warns(warns); - print_errors(errors); - println!(); - - Err(anyhow!(message)) - } - } -} - -fn is_up_to_date(skip_check: bool, sources: FS, targets: FT) -> bool -where - S: IntoIterator, - T: IntoIterator, - FS: FnOnce() -> S, - FT: FnOnce() -> T, -{ - if skip_check { - return false; - } - - fn max_modified(path: &Path) -> Option { - let mut max_modified: Option = None; - let mut update_max_modified = |modified: SystemTime| { - if max_modified.map_or(true, |max_mod| max_mod.cmp(&modified) == Ordering::Less) { - max_modified = Some(modified) - } - }; - - if let Ok(metadata) = std::fs::metadata(path) { - if metadata.is_dir() { - WalkDir::new(path) - .into_iter() - .filter_map(|entry| entry.ok().and_then(|entry| entry.metadata().ok())) - .filter(|metadata| !metadata.is_dir()) - .filter_map(|metadata| metadata.modified().ok()) - .for_each(update_max_modified) - } else if let Ok(modified) = metadata.modified() { - update_max_modified(modified) - } - } - - max_modified - } - - fn max_modified_short_circuit_on_missing>( - paths: I, - ) -> Option { - // Using Result and collect for short-circuit on any missing mod time - paths - .into_iter() - .map(|path| max_modified(path.as_path()).ok_or(())) - .collect::, _>>() - .and_then(|mod_times| mod_times.into_iter().max().ok_or(())) - .ok() - } - - let targets = targets(); - - let max_target_modified = max_modified_short_circuit_on_missing(targets); - - let max_target_modified = match max_target_modified { - Some(modified) => modified, - None => return false, - }; - - let sources = sources(); - - let max_source_modified = max_modified_short_circuit_on_missing(sources); - - match max_source_modified { - Some(max_source_modified) => { - max_source_modified.cmp(&max_target_modified) == Ordering::Less - } - None => false, - } -} - -fn compile_and_collect_globs(root_dir: &Path, globs: &[String]) -> Result, Error> { - globs - .iter() - .map(|pattern| { - glob(&format!("{}/{}", root_dir.to_string_lossy(), pattern)) - .with_context(|| format!("Failed to compile glob expression: {}", pattern)) - }) - .collect::, _>>() - .map_err(|err| anyhow!(err)) - .and_then(|paths| { - paths - .into_iter() - .flatten() - .collect::, _>>() - .map_err(|err| anyhow!(err)) - }) -} diff --git a/wasm-rpc-stubgen/src/commands/dependencies.rs b/wasm-rpc-stubgen/src/commands/dependencies.rs index 1c1223af..1ef9802f 100644 --- a/wasm-rpc-stubgen/src/commands/dependencies.rs +++ b/wasm-rpc-stubgen/src/commands/dependencies.rs @@ -12,156 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::commands::log::{log_action_plan, log_warn_action}; -use crate::fs::{get_file_name, strip_path_prefix, OverwriteSafeAction, OverwriteSafeActions}; -use crate::wit::{generate_stub_wit_from_wit_dir, import_remover}; -use crate::wit_resolve::ResolvedWitDir; -use crate::{cargo, naming}; -use anyhow::{anyhow, Context}; -use std::collections::BTreeMap; -use std::path::{Path, PathBuf}; -use wit_parser::PackageName; +use crate::wit_generate; +use crate::wit_generate::AddStubAsDepConfig; +use std::path::Path; -#[derive(PartialEq, Eq)] -pub enum UpdateCargoToml { - Update, - UpdateIfExists, - NoUpdate, -} +pub use crate::wit_generate::UpdateCargoToml; pub fn add_stub_dependency( stub_wit_root: &Path, dest_wit_root: &Path, - overwrite: bool, update_cargo_toml: UpdateCargoToml, ) -> anyhow::Result<()> { - let stub_resolved_wit_root = ResolvedWitDir::new(stub_wit_root)?; - let stub_package = stub_resolved_wit_root.main_package()?; - let stub_wit = stub_wit_root.join(naming::wit::STUB_WIT_FILE_NAME); - - let dest_deps_dir = dest_wit_root.join(naming::wit::DEPS_DIR); - let dest_resolved_wit_root = ResolvedWitDir::new(dest_wit_root)?; - let dest_package = dest_resolved_wit_root.main_package()?; - let dest_stub_package_name = naming::wit::stub_package_name(&dest_package.name); - let dest_stub_import_remover = import_remover(&dest_stub_package_name); - - { - let is_self_stub_by_name = - dest_package.name == naming::wit::stub_target_package_name(&stub_package.name); - let is_self_stub_by_content = is_self_stub(&stub_wit, dest_wit_root); - - if is_self_stub_by_name && !is_self_stub_by_content? { - return Err(anyhow!( - "Both the caller and the target components are using the same package name ({}), which is not supported.", - dest_package.name - )); - } - } - - let mut actions = OverwriteSafeActions::new(); - let mut package_names_to_package_path = BTreeMap::::new(); - - for (package_name, package_id) in &stub_resolved_wit_root.resolve.package_names { - let (package_path, package_sources) = stub_resolved_wit_root - .sources - .get(package_id) - .ok_or_else(|| anyhow!("Failed to get package sources for {}", package_name))?; - let package_path = - naming::wit::package_wit_dep_dir_from_package_dir_name(&get_file_name(package_path)?); - - let is_stub_main_package = *package_id == stub_resolved_wit_root.package_id; - let is_dest_package = *package_name == dest_package.name; - let is_dest_stub_package = *package_name == dest_stub_package_name; - - // We skip self as a dependency - if is_dest_package { - log_warn_action( - "Skipping", - format!("cyclic self dependency for {}", package_name), - ); - } else if is_dest_stub_package || is_stub_main_package { - let package_dep_dir_name = naming::wit::package_dep_dir_name(package_name); - let package_path = naming::wit::package_wit_dep_dir_from_package_name(package_name); - - package_names_to_package_path.insert(package_name.clone(), package_path); - - // Handle self stub packages: use regenerated stub with inlining, to break the recursive cycle - if is_dest_stub_package { - actions.add(OverwriteSafeAction::WriteFile { - content: generate_stub_wit_from_wit_dir(dest_wit_root, true)?, - target: dest_deps_dir - .join(&package_dep_dir_name) - .join(naming::wit::STUB_WIT_FILE_NAME), - }); - // Non-self stub package has to be copied into target deps - } else { - for source in package_sources { - actions.add(OverwriteSafeAction::CopyFile { - source: source.clone(), - target: dest_deps_dir - .join(&package_dep_dir_name) - .join(get_file_name(source)?), - }); - } - } - // Handle other package by copying while removing imports - } else { - package_names_to_package_path.insert(package_name.clone(), package_path); - - for source in package_sources { - actions.add(OverwriteSafeAction::copy_file_transformed( - source.clone(), - dest_wit_root.join(strip_path_prefix(stub_wit_root, source)?), - &dest_stub_import_remover, - )?); - } - } - } - - let forbidden_overwrites = actions.run(overwrite, log_action_plan)?; - if !forbidden_overwrites.is_empty() { - eprintln!("The following files would have been overwritten with new content:"); - for action in forbidden_overwrites { - eprintln!(" {}", action.target().to_string_lossy()); - } - eprintln!(); - eprintln!("Use --overwrite to force overwrite."); - } - - if let Some(target_parent) = dest_wit_root.parent() { - let target_cargo_toml = target_parent.join("Cargo.toml"); - if target_cargo_toml.exists() && target_cargo_toml.is_file() { - if update_cargo_toml == UpdateCargoToml::NoUpdate { - eprintln!("Warning: the newly copied dependencies have to be added to {}. Use the --update-cargo-toml flag to update it automatically.", target_cargo_toml.to_string_lossy()); - } else { - cargo::is_cargo_component_toml(&target_cargo_toml).context(format!( - "The file {target_cargo_toml:?} is not a valid cargo-component project" - ))?; - cargo::add_dependencies_to_cargo_toml( - &target_cargo_toml, - package_names_to_package_path, - )?; - } - } else if update_cargo_toml == UpdateCargoToml::Update { - return Err(anyhow!( - "Cannot update {:?} file because it does not exist or is not a file", - target_cargo_toml - )); - } - } else if update_cargo_toml == UpdateCargoToml::Update { - return Err(anyhow!("Cannot update the Cargo.toml file because parent directory of the destination WIT root does not exist.")); - } - - Ok(()) -} - -/// Checks whether `stub_wit` is a stub generated for `dest_wit_root` -fn is_self_stub(stub_wit: &Path, dest_wit_root: &Path) -> anyhow::Result { - // TODO: can we make it diff exports instead of generated content? - let dest_stub_wit_imported = generate_stub_wit_from_wit_dir(dest_wit_root, false)?; - let dest_stub_wit_inlined = generate_stub_wit_from_wit_dir(dest_wit_root, true)?; - let stub_wit = std::fs::read_to_string(stub_wit)?; - - // TODO: this can also be false in case the stub is lagging - Ok(stub_wit == dest_stub_wit_imported || stub_wit == dest_stub_wit_inlined) + wit_generate::add_stub_as_dependency_to_wit_dir(AddStubAsDepConfig { + stub_wit_root: stub_wit_root.to_path_buf(), + dest_wit_root: dest_wit_root.to_path_buf(), + update_cargo_toml, + }) } diff --git a/wasm-rpc-stubgen/src/commands/generate.rs b/wasm-rpc-stubgen/src/commands/generate.rs index 456f4940..ed16b899 100644 --- a/wasm-rpc-stubgen/src/commands/generate.rs +++ b/wasm-rpc-stubgen/src/commands/generate.rs @@ -14,16 +14,16 @@ use crate::cargo::generate_cargo_toml; use crate::compilation::compile; -use crate::fs::copy; +use crate::fs; +use crate::log::{log_action, LogColorize, LogIndent}; use crate::naming; use crate::rust::generate_stub_source; use crate::stub::StubDefinition; -use crate::wit::{copy_wit_dependencies, generate_stub_wit_to_target}; +use crate::wit_generate::{add_dependencies_to_stub_wit_dir, generate_stub_wit_to_target}; use crate::wit_resolve::ResolvedWitDir; -use anyhow::Context; +use anyhow::{anyhow, Context}; use fs_extra::dir::CopyOptions; use heck::ToSnakeCase; -use std::fs; use std::path::{Path, PathBuf}; pub fn generate(stub_def: &StubDefinition) -> anyhow::Result<()> { @@ -37,15 +37,15 @@ pub async fn build( stub_def: &StubDefinition, dest_wasm: &Path, dest_wit_root: &Path, + offline: bool, ) -> anyhow::Result<()> { - let wasm_path = generate_and_build_stub(stub_def).await?; - - copy(wasm_path, dest_wasm).context("Failed to copy the WASM file to the destination")?; + let wasm_path = generate_and_build_stub(stub_def, offline).await?; + fs::copy(wasm_path, dest_wasm).context("Failed to copy the WASM file to the destination")?; fs::create_dir_all(dest_wit_root).context("Failed to create the target WIT root directory")?; fs_extra::dir::copy( - stub_def.target_root.join(naming::wit::WIT_DIR), + stub_def.config.target_root.join(naming::wit::WIT_DIR), dest_wit_root, &CopyOptions::new().content_only(true).overwrite(true), ) @@ -54,24 +54,32 @@ pub async fn build( Ok(()) } -pub fn generate_stub_wit_dir(stub_def: &StubDefinition) -> anyhow::Result { - generate_stub_wit_to_target(stub_def).context("Failed to generate the stub wit file")?; - copy_wit_dependencies(stub_def).context("Failed to copy the dependent wit files")?; - stub_def - .resolve_target_wit() - .context("Failed to resolve the result WIT root") -} - -pub async fn generate_and_build_stub(stub_def: &StubDefinition) -> anyhow::Result { +pub async fn generate_and_build_stub( + stub_def: &StubDefinition, + offline: bool, +) -> anyhow::Result { let _ = generate_stub_wit_dir(stub_def)?; generate_cargo_toml(stub_def).context("Failed to generate the Cargo.toml file")?; generate_stub_source(stub_def).context("Failed to generate the stub Rust source")?; - compile(&stub_def.target_root) - .await - .context("Failed to compile the generated stub")?; + compile( + &stub_def + .config + .target_root + .canonicalize() + .with_context(|| { + anyhow!( + "Failed to canonicalize stub target root {}", + stub_def.config.target_root.log_color_error_highlight() + ) + })?, + offline, + ) + .await + .context("Failed to compile the generated stub")?; let wasm_path = stub_def + .config .target_root .join("target") .join("wasm32-wasi") @@ -82,3 +90,19 @@ pub async fn generate_and_build_stub(stub_def: &StubDefinition) -> anyhow::Resul )); Ok(wasm_path) } + +pub fn generate_stub_wit_dir(stub_def: &StubDefinition) -> anyhow::Result { + log_action( + "Generating", + format!( + "stub WIT directory to {}", + stub_def.config.target_root.log_color_highlight() + ), + ); + let _indent = LogIndent::new(); + generate_stub_wit_to_target(stub_def).context("Failed to generate the stub wit file")?; + add_dependencies_to_stub_wit_dir(stub_def).context("Failed to copy the dependent wit files")?; + stub_def + .resolve_target_wit() + .context("Failed to resolve the result WIT root") +} diff --git a/wasm-rpc-stubgen/src/commands/log.rs b/wasm-rpc-stubgen/src/commands/log.rs deleted file mode 100644 index 4fe87125..00000000 --- a/wasm-rpc-stubgen/src/commands/log.rs +++ /dev/null @@ -1,114 +0,0 @@ -use crate::fs::{OverwriteSafeAction, OverwriteSafeActionPlan}; -use crate::model::validation::ValidatedResult; -use colored::Colorize; - -pub fn log_action>(action: &str, subject: T) { - println!("{} {}", action.green(), subject.as_ref()) -} - -pub fn log_warn_action>(action: &str, subject: T) { - println!("{} {}", action.yellow(), subject.as_ref()) -} - -pub fn log_skipping_up_to_date>(subject: T) { - log_warn_action( - "Skipping", - format!("{}, already up-to-date", subject.as_ref()), - ); -} - -pub fn log_validated_action_result(action: &str, result: &ValidatedResult, to_log: F) -where - F: Fn(&T) -> String, -{ - result - .as_ok_ref() - .iter() - .for_each(|value| log_action(action, to_log(value))); -} - -pub fn log_action_plan(action: &OverwriteSafeAction, plan: OverwriteSafeActionPlan) { - match plan { - OverwriteSafeActionPlan::Create => match action { - OverwriteSafeAction::CopyFile { source, target } => { - log_action( - "Copying", - format!( - "{} to {}", - source.to_string_lossy(), - target.to_string_lossy() - ), - ); - } - OverwriteSafeAction::CopyFileTransformed { source, target, .. } => { - log_action( - "Copying", - format!( - "{} to {} transformed", - source.to_string_lossy(), - target.to_string_lossy() - ), - ); - } - OverwriteSafeAction::WriteFile { target, .. } => { - log_action("Creating", format!("{}", target.to_string_lossy())); - } - }, - OverwriteSafeActionPlan::Overwrite => match action { - OverwriteSafeAction::CopyFile { source, target } => { - log_warn_action( - "Overwriting", - format!( - "{} with {}", - target.to_string_lossy(), - source.to_string_lossy() - ), - ); - } - OverwriteSafeAction::CopyFileTransformed { source, target, .. } => { - log_warn_action( - "Overwriting", - format!( - "{} with {} transformed", - target.to_string_lossy(), - source.to_string_lossy() - ), - ); - } - OverwriteSafeAction::WriteFile { content: _, target } => { - log_warn_action("Overwriting", format!("{}", target.to_string_lossy())); - } - }, - OverwriteSafeActionPlan::SkipSameContent => match action { - OverwriteSafeAction::CopyFile { source, target } => { - log_warn_action( - "Skipping", - format!( - "copying {} to {}, content already up-to-date", - source.to_string_lossy(), - target.to_string_lossy(), - ), - ); - } - OverwriteSafeAction::CopyFileTransformed { source, target, .. } => { - log_warn_action( - "Skipping", - format!( - "copying {} to {} transformed, content already up-to-date", - source.to_string_lossy(), - target.to_string_lossy() - ), - ); - } - OverwriteSafeAction::WriteFile { content: _, target } => { - log_warn_action( - "Skipping", - format!( - "generating {}, content already up-to-date", - target.to_string_lossy() - ), - ); - } - }, - } -} diff --git a/wasm-rpc-stubgen/src/commands/mod.rs b/wasm-rpc-stubgen/src/commands/mod.rs index aa03f6aa..573e0f24 100644 --- a/wasm-rpc-stubgen/src/commands/mod.rs +++ b/wasm-rpc-stubgen/src/commands/mod.rs @@ -22,7 +22,4 @@ pub mod dependencies; pub mod composition; /// Declarative subcommands -pub mod declarative; - -/// Logging -pub mod log; +pub mod app; diff --git a/wasm-rpc-stubgen/src/compilation.rs b/wasm-rpc-stubgen/src/compilation.rs index 20d296eb..3b0ca445 100644 --- a/wasm-rpc-stubgen/src/compilation.rs +++ b/wasm-rpc-stubgen/src/compilation.rs @@ -17,13 +17,14 @@ use cargo_component::{load_component_metadata, load_metadata, run_cargo_command} use cargo_component_core::terminal::{Color, Terminal, Verbosity}; use std::path::Path; -pub async fn compile(root: &Path) -> anyhow::Result<()> { +pub async fn compile(root: &Path, offline: bool) -> anyhow::Result<()> { let current_dir = std::env::current_dir()?; std::env::set_current_dir(root)?; let cargo_args = CargoArguments { release: true, manifest_path: Some(root.join("Cargo.toml")), + offline, ..Default::default() }; @@ -33,13 +34,18 @@ pub async fn compile(root: &Path) -> anyhow::Result<()> { let packages = load_component_metadata(&metadata, cargo_args.packages.iter(), cargo_args.workspace)?; + let mut spawn_args = vec!["build".to_string(), "--release".to_string()]; + if offline { + spawn_args.push("--offline".to_string()); + } + run_cargo_command( &config, &metadata, &packages, Some("build"), &cargo_args, - &["build".to_string(), "--release".to_string()], + &spawn_args, ) .await?; diff --git a/wasm-rpc-stubgen/src/fs.rs b/wasm-rpc-stubgen/src/fs.rs index 8ebe3d1e..dfc0a28a 100644 --- a/wasm-rpc-stubgen/src/fs.rs +++ b/wasm-rpc-stubgen/src/fs.rs @@ -1,39 +1,40 @@ +use crate::log::LogColorize; use anyhow::{anyhow, Context}; use std::cmp::PartialEq; +use std::fs::Metadata; use std::path::{Path, PathBuf}; use std::time::SystemTime; +pub fn create_dir_all>(path: P) -> anyhow::Result<()> { + let path = path.as_ref(); + if path.exists() { + Ok(()) + } else { + std::fs::create_dir_all(path) + .with_context(|| anyhow!("Failed to create directory {}", path.log_color_highlight())) + } +} + // Differences compared to std::fs::copy // - ensures that the target dir exists // - updated the modtime after copy, which is not guaranteed to happen, making it not usable for // modtime based up-to-date checks (see https://github.com/rust-lang/rust/issues/115982 for more info) // - uses anyhow error with added context pub fn copy, Q: AsRef>(from: P, to: Q) -> anyhow::Result { - let from = from.as_ref(); - let to = to.as_ref(); - - let context = || { - format!( - "Failed to copy from {} to {}", - from.to_string_lossy(), - to.to_string_lossy() - ) - }; + let from = PathExtra(from); + let to = PathExtra(to); - let target_parent = to - .parent() - .ok_or_else(|| anyhow!("Failed to get target parent dir")) - .with_context(context)?; + let context = || format!("Failed to copy from {} to {}", from.display(), to.display()); - std::fs::create_dir_all(target_parent) - .with_context(|| anyhow!("Failed to create target dir")) + create_dir_all(to.parent()?) + .context("Failed to create target dir") .with_context(context)?; - let bytes = std::fs::copy(from, to).with_context(context)?; + let bytes = std::fs::copy(&from, &to).with_context(context)?; - std::fs::File::open(to) + std::fs::File::open(&to) .and_then(|to| to.set_modified(SystemTime::now())) - .with_context(|| anyhow!("Failed to update target modification time")) + .context("Failed to update target modification time") .with_context(context)?; Ok(bytes) @@ -45,61 +46,86 @@ pub fn copy_transformed, Q: AsRef, T: Fn(String) -> anyhow: to: Q, transform: T, ) -> anyhow::Result { - let from = from.as_ref(); - let to = to.as_ref(); + let from = PathExtra(from); + let to = PathExtra(to); let context = || { format!( "Failed to copy (and transform) from {} to {}", - from.to_string_lossy(), - to.to_string_lossy() + from.display(), + to.display() ) }; - let target_parent = to - .parent() - .ok_or_else(|| anyhow!("Failed to get target parent dir")) - .with_context(context)?; - - std::fs::create_dir_all(target_parent) - .with_context(|| anyhow!("Failed to create target dir")) + create_dir_all(from.parent()?) + .context("Failed to create target dir") .with_context(context)?; - let content = std::fs::read_to_string(from) - .with_context(|| anyhow!("Failed to read source content")) - .with_context(context)?; + let content = read_to_string(&from).with_context(context)?; let transformed_content = transform(content) - .with_context(|| anyhow!("Failed to transform source content")) + .context("Failed to transform source content") .with_context(context)?; let bytes_count = transformed_content.as_bytes().len(); - std::fs::write(to, transformed_content.as_bytes()) - .with_context(|| anyhow!("Failed to write transformed content")) + write(&to, transformed_content.as_bytes()) + .context("Failed to write transformed content") .with_context(context)?; Ok(bytes_count as u64) } +pub fn read_to_string>(path: P) -> anyhow::Result { + let path = path.as_ref(); + fs_extra::file::read_to_string(path).with_context(|| { + anyhow!( + "Failed to read to string, file: {}", + path.log_color_highlight() + ) + }) +} + +pub fn read>(path: P) -> anyhow::Result> { + let path = path.as_ref(); + std::fs::read(path) + .with_context(|| anyhow!("Failed to read file: {}", path.log_color_highlight())) +} + // Creates all missing parent directories if necessary and writes str to path. pub fn write_str, S: AsRef>(path: P, str: S) -> anyhow::Result<()> { - let path = path.as_ref(); + let path = PathExtra(path); let str = str.as_ref(); - let context = || format!("Failed to write string to {}", path.to_string_lossy()); + let context = || anyhow!("Failed to write string to {}", path.log_color_highlight()); - let target_parent = path - .parent() - .ok_or_else(|| anyhow!("Failed to get parent dir")) - .with_context(context)?; + let target_parent = path.parent().with_context(context)?; + create_dir_all(target_parent).with_context(context)?; + std::fs::write(&path, str.as_bytes()).with_context(context) +} - std::fs::create_dir_all(target_parent) - .with_context(|| anyhow!("Failed to create parent dir")) - .with_context(context)?; +pub fn write, C: AsRef<[u8]>>(path: P, contents: C) -> anyhow::Result<()> { + let path = PathExtra(path); - std::fs::write(path, str.as_bytes()).with_context(context)?; + let context = || anyhow!("Failed to write to {}", path.log_color_highlight()); + let target_parent = path.parent().with_context(context)?; + create_dir_all(target_parent).with_context(context)?; + std::fs::write(&path, contents).with_context(context) +} + +pub fn remove>(path: P) -> anyhow::Result<()> { + let path = path.as_ref(); + if path.exists() { + if path.is_dir() { + std::fs::remove_dir_all(path).with_context(|| { + anyhow!("Failed to delete directory {}", path.log_color_highlight()) + })?; + } else { + std::fs::remove_file(path) + .with_context(|| anyhow!("Failed to delete file {}", path.log_color_highlight()))?; + } + } Ok(()) } @@ -108,14 +134,14 @@ pub fn has_str_content, S: AsRef>(path: P, str: S) -> anyhow let str = str.as_ref(); let context = || { - format!( + anyhow!( "Failed to compare content to string for {}", - path.to_string_lossy() + path.log_color_highlight() ) }; - let content = std::fs::read_to_string(path) - .with_context(|| anyhow!("Failed to read as string: {}", path.to_string_lossy())) + let content = read_to_string(path) + .with_context(|| anyhow!("Failed to read as string: {}", path.log_color_highlight())) .with_context(context)?; Ok(content == str) @@ -126,60 +152,104 @@ pub fn has_same_string_content, Q: AsRef>(a: P, b: Q) -> an let b = b.as_ref(); let context = || { - format!( + anyhow!( "Failed to compare string contents of {} and {}", - a.to_string_lossy(), - b.to_string_lossy() + a.log_color_highlight(), + b.log_color_highlight() ) }; - let content_a = std::fs::read_to_string(a) - .with_context(|| anyhow!("Failed to read as string: {}", a.to_string_lossy())) - .with_context(context)?; - - let content_b = std::fs::read_to_string(b) - .with_context(|| anyhow!("Failed to read as string: {}", b.to_string_lossy())) - .with_context(context)?; + let content_a = read_to_string(a).with_context(context)?; + let content_b = read_to_string(b).with_context(context)?; Ok(content_a == content_b) } -pub fn get_file_name>(path: P) -> anyhow::Result { +pub fn metadata>(path: P) -> anyhow::Result { let path = path.as_ref(); - path.file_name() - .ok_or_else(|| { - anyhow!( - "Failed to get file name for package source: {}", - path.to_string_lossy(), - ) - })? - .to_os_string() - .into_string() - .map_err(|_| { + std::fs::metadata(path) + .with_context(|| anyhow!("Failed to get metadata for {}", path.log_color_highlight())) +} + +pub struct PathExtra>(P); + +impl> PathExtra

{ + pub fn new(path: P) -> Self { + Self(path) + } + + pub fn parent(&self) -> anyhow::Result<&Path> { + let path = self.0.as_ref(); + path.parent().ok_or_else(|| { anyhow!( - "Failed to convert filename for path: {}", - path.to_string_lossy() + "Failed to get parent dir for path: {}", + path.log_color_highlight() ) }) -} + } -pub fn strip_path_prefix, Q: AsRef>( - prefix: Q, - path: P, -) -> anyhow::Result { - let path = path.as_ref(); - let prefix = prefix.as_ref(); + pub fn file_name_to_string(&self) -> anyhow::Result { + let path = self.0.as_ref(); + path.file_name() + .ok_or_else(|| { + anyhow!( + "Failed to get file name for path: {}", + path.log_color_highlight(), + ) + })? + .to_os_string() + .into_string() + .map_err(|_| { + anyhow!( + "Failed to convert filename for path: {}", + path.log_color_highlight() + ) + }) + } - Ok(path - .strip_prefix(prefix) - .with_context(|| { + pub fn to_str(&self) -> anyhow::Result<&str> { + let path = self.0.as_ref(); + path.to_str().ok_or_else(|| { anyhow!( - "Failed to strip prefix from path, prefix: {}, path: {}", - prefix.to_string_lossy(), - path.to_string_lossy() + "Failed to convert path to string: {}", + path.log_color_highlight() ) - })? - .to_path_buf()) + }) + } + + pub fn to_string(&self) -> anyhow::Result { + Ok(self.to_str()?.to_string()) + } + + pub fn strip_prefix>(&self, prefix: Q) -> anyhow::Result { + let path = self.0.as_ref(); + let prefix = prefix.as_ref(); + + Ok(path + .strip_prefix(prefix) + .with_context(|| { + anyhow!( + "Failed to strip prefix from path, prefix: {}, path: {}", + prefix.log_color_highlight(), + path.log_color_highlight() + ) + })? + .to_path_buf()) + } + + pub fn as_path(&self) -> &Path { + self.0.as_ref() + } + + pub fn display(&self) -> std::path::Display { + self.as_path().display() + } +} + +impl> AsRef for PathExtra

{ + fn as_ref(&self) -> &Path { + self.as_path() + } } pub enum OverwriteSafeAction { @@ -205,19 +275,19 @@ impl OverwriteSafeAction { transform: F, ) -> anyhow::Result where - F: Fn(String) -> anyhow::Result, + F: FnOnce(String) -> anyhow::Result, { let content = std::fs::read_to_string(&source).with_context(|| { anyhow!( "Failed to read file as string, path: {}", - source.to_string_lossy() + source.log_color_highlight() ) })?; let source_transformed = transform(content).with_context(|| { anyhow!( "Failed to transform file, path: {}", - source.to_string_lossy() + source.log_color_highlight() ) })?; @@ -269,6 +339,7 @@ impl OverwriteSafeActions { pub fn run( self, allow_overwrite: bool, + allow_skip_by_content: bool, log_action: F, ) -> anyhow::Result> where @@ -281,23 +352,28 @@ impl OverwriteSafeActions { for action in self.0 { let plan = match &action { - OverwriteSafeAction::CopyFile { source, target } => { - Self::plan_for_action(allow_overwrite, target, || { - has_same_string_content(source, target) - })? - } + OverwriteSafeAction::CopyFile { source, target } => Self::plan_for_action( + allow_overwrite, + allow_skip_by_content, + target, + || has_same_string_content(source, target), + )?, OverwriteSafeAction::CopyFileTransformed { source_content_transformed: source_transformed, target, .. - } => Self::plan_for_action(allow_overwrite, target, || { - has_str_content(target, source_transformed) - })?, - OverwriteSafeAction::WriteFile { content, target } => { - Self::plan_for_action(allow_overwrite, target, || { - has_str_content(target, content) - })? - } + } => Self::plan_for_action( + allow_overwrite, + allow_skip_by_content, + target, + || has_str_content(target, source_transformed), + )?, + OverwriteSafeAction::WriteFile { content, target } => Self::plan_for_action( + allow_overwrite, + allow_skip_by_content, + target, + || has_str_content(target, content), + )?, }; match plan { Some(plan) => actions_with_plan.push((action, plan)), @@ -340,16 +416,17 @@ impl OverwriteSafeActions { fn plan_for_action( allow_overwrite: bool, + allow_skip_by_content: bool, target: P, skip_by_content: F, ) -> anyhow::Result> where P: AsRef, - F: Fn() -> anyhow::Result, + F: FnOnce() -> anyhow::Result, { if !target.as_ref().exists() { Ok(Some(OverwriteSafeActionPlan::Create)) - } else if skip_by_content()? { + } else if allow_skip_by_content && skip_by_content()? { Ok(Some(OverwriteSafeActionPlan::SkipSameContent)) } else if allow_overwrite { Ok(Some(OverwriteSafeActionPlan::Overwrite)) diff --git a/wasm-rpc-stubgen/src/lib.rs b/wasm-rpc-stubgen/src/lib.rs index 52773684..b6c3ec5c 100644 --- a/wasm-rpc-stubgen/src/lib.rs +++ b/wasm-rpc-stubgen/src/lib.rs @@ -16,18 +16,23 @@ pub mod cargo; pub mod commands; pub mod compilation; pub mod fs; +pub mod log; pub mod make; pub mod model; pub mod naming; pub mod rust; pub mod stub; -pub mod wit; +pub mod validation; +pub mod wit_encode; +pub mod wit_generate; pub mod wit_resolve; -use crate::commands::dependencies::UpdateCargoToml; -use crate::stub::StubDefinition; +use crate::model::app::{ComponentPropertiesExtensions, ComponentPropertiesExtensionsAny}; +use crate::stub::{StubConfig, StubDefinition}; +use crate::wit_generate::UpdateCargoToml; use anyhow::Context; use clap::{Parser, Subcommand}; +use std::marker::PhantomData; use std::path::PathBuf; use tempfile::TempDir; @@ -48,8 +53,8 @@ pub enum Command { /// Initializes a Golem-specific cargo-make configuration in a Cargo workspace for automatically /// generating stubs and composing results. InitializeWorkspace(InitializeWorkspaceArgs), - /// Build components and stubs with application manifests - #[cfg(feature = "unstable-dec-dep")] + /// Build components with application manifests + #[cfg(feature = "app-command")] App { #[command(subcommand)] subcommand: App, @@ -81,7 +86,7 @@ pub struct GenerateArgs { /// it from the stub WIT. This is useful for example with ComponentizeJS currently where otherwise /// the original component's interface would be added as an import to the final WASM. #[clap(long, default_value_t = false)] - pub always_inline_types: bool, + pub always_inline_types: bool, // TODO: deprecated } #[derive(clap::Args, Debug, Clone)] @@ -127,7 +132,7 @@ pub struct BuildArgs { /// it from the stub WIT. This is useful for example with ComponentizeJS currently where otherwise /// the original component's interface would be added as an import to the final WASM. #[clap(long, default_value_t = false)] - pub always_inline_types: bool, + pub always_inline_types: bool, // TODO: deprecated } /// Adds a generated stub as a dependency to another WASM component @@ -145,7 +150,7 @@ pub struct AddStubDependencyArgs { /// This command would not do anything if it detects that it would change an existing WIT file's contents at /// the destination. With this flag, it can be forced to overwrite those files. #[clap(short, long)] - pub overwrite: bool, + pub overwrite: bool, // TODO: deprecate /// Enables updating the Cargo.toml file in the parent directory of `dest-wit-root` with the copied /// dependencies. #[clap(short, long)] @@ -187,44 +192,60 @@ pub struct InitializeWorkspaceArgs { #[derive(Subcommand, Debug)] pub enum App { - /// Creates application manifest for component - Init(DeclarativeInitArgs), - /// Runs the pre-component-build steps (stub generation and adding wit dependencies) - PreComponentBuild(DeclarativeBuildArgs), /// Runs component build steps - ComponentBuild(DeclarativeBuildArgs), - /// Runs the post-component-build steps (composing stubs) - PostComponentBuild(DeclarativeBuildArgs), - /// Runs all build steps (pre-component, component, post-component) - Build(DeclarativeBuildArgs), + Build(AppBuildArgs), + /// Clean outputs + Clean(AppCleanArgs), + /// Run custom command + #[clap(external_subcommand)] + CustomCommand(Vec), } #[derive(clap::Args, Debug)] #[command(version, about, long_about = None)] -pub struct DeclarativeInitArgs { - #[clap(long, short, required = true)] - pub component_name: String, +pub struct AppBuildArgs { + /// List of application manifests, can be defined multiple times + #[clap(long, short)] + pub app: Vec, + /// When set to true will skip modification time based up-to-date checks, defaults to false + #[clap(long, short, default_value = "false")] + pub force_build: bool, + /// Selects a build profile + #[clap(long, short)] + pub profile: Option, + /// When set to true will use offline mode where applicable (e.g. stub cargo builds), defaults to false + #[clap(long, short, default_value = "false")] + pub offline: bool, } #[derive(clap::Args, Debug)] #[command(version, about, long_about = None)] -pub struct DeclarativeBuildArgs { +pub struct AppCleanArgs { /// List of application manifests, can be defined multiple times #[clap(long, short)] pub app: Vec, - /// When set to true will skip modification time based up-to-date checks, defaults to false - #[clap(long, short, default_value = "false")] - pub force_build: bool, +} + +#[derive(clap::Args, Debug)] +#[command(version, about, long_about = None)] +pub struct AppCustomCommand { + #[clap(flatten)] + args: AppBuildArgs, + #[arg(value_name = "custom command")] + command: String, } pub fn generate(args: GenerateArgs) -> anyhow::Result<()> { let stub_def = StubDefinition::new( - &args.source_wit_root, - &args.dest_crate_root, - &args.world, - &args.stub_crate_version, - &args.wasm_rpc_override, - args.always_inline_types, + StubConfig { + source_wit_root: args.source_wit_root, + target_root: args.dest_crate_root, + selected_world: args.world, + stub_crate_version: args.stub_crate_version, + wasm_rpc_override: args.wasm_rpc_override, + extract_source_interface_package: true, + seal_cargo_workspace: false, + } ) .context("Failed to gather information for the stub generator. Make sure source_wit_root has a valid WIT file.")?; commands::generate::generate(&stub_def) @@ -232,26 +253,25 @@ pub fn generate(args: GenerateArgs) -> anyhow::Result<()> { pub async fn build(args: BuildArgs) -> anyhow::Result<()> { let target_root = TempDir::new()?; - let canonical_target_root = target_root.path().canonicalize()?; - let stub_def = StubDefinition::new( - &args.source_wit_root, - &canonical_target_root, - &args.world, - &args.stub_crate_version, - &args.wasm_rpc_override, - args.always_inline_types, - ) + let stub_def = StubDefinition::new(StubConfig { + source_wit_root: args.source_wit_root, + target_root: target_root.path().to_path_buf(), + selected_world: args.world, + stub_crate_version: args.stub_crate_version, + wasm_rpc_override: args.wasm_rpc_override, + extract_source_interface_package: true, + seal_cargo_workspace: false, + }) .context("Failed to gather information for the stub generator")?; - commands::generate::build(&stub_def, &args.dest_wasm, &args.dest_wit_root).await + commands::generate::build(&stub_def, &args.dest_wasm, &args.dest_wit_root, false).await } pub fn add_stub_dependency(args: AddStubDependencyArgs) -> anyhow::Result<()> { commands::dependencies::add_stub_dependency( &args.stub_wit_root, &args.dest_wit_root, - args.overwrite, if args.update_cargo_toml { UpdateCargoToml::Update } else { @@ -278,31 +298,41 @@ pub fn initialize_workspace( ) } -pub async fn run_declarative_command(command: App) -> anyhow::Result<()> { +pub async fn run_app_command( + command: App, +) -> anyhow::Result<()> { match command { - App::Init(args) => commands::declarative::init(args.component_name), - App::PreComponentBuild(args) => { - commands::declarative::pre_component_build(dec_build_args_to_config(args)).await + App::Build(args) => { + commands::app::build(commands::app::Config { + app_resolve_mode: app_manifest_sources_to_resolve_mode(args.app), + skip_up_to_date_checks: args.force_build, + profile: args.profile.map(|profile| profile.into()), + offline: args.offline, + extensions: PhantomData::, + }) + .await } - App::ComponentBuild(args) => { - commands::declarative::component_build(dec_build_args_to_config(args)) + App::Clean(args) => commands::app::clean(commands::app::Config { + app_resolve_mode: app_manifest_sources_to_resolve_mode(args.app), + skip_up_to_date_checks: false, + profile: None, + offline: false, + extensions: PhantomData::, + }), + App::CustomCommand(_args) => { + // TODO: parse app manifest / profile args + // commands::app::custom_command(app_args_to_config(args.args), args.command) + Ok(()) } - App::PostComponentBuild(args) => { - commands::declarative::post_component_build(dec_build_args_to_config(args)).await - } - App::Build(args) => commands::declarative::build(dec_build_args_to_config(args)).await, } } -fn dec_build_args_to_config(args: DeclarativeBuildArgs) -> commands::declarative::Config { - commands::declarative::Config { - app_resolve_mode: { - if args.app.is_empty() { - commands::declarative::ApplicationResolveMode::Automatic - } else { - commands::declarative::ApplicationResolveMode::Explicit(args.app) - } - }, - skip_up_to_date_checks: args.force_build, +fn app_manifest_sources_to_resolve_mode( + sources: Vec, +) -> commands::app::ApplicationSourceMode { + if sources.is_empty() { + commands::app::ApplicationSourceMode::Automatic + } else { + commands::app::ApplicationSourceMode::Explicit(sources) } } diff --git a/wasm-rpc-stubgen/src/log.rs b/wasm-rpc-stubgen/src/log.rs new file mode 100644 index 00000000..eff0d723 --- /dev/null +++ b/wasm-rpc-stubgen/src/log.rs @@ -0,0 +1,244 @@ +use crate::fs::{OverwriteSafeAction, OverwriteSafeActionPlan, PathExtra}; +use crate::validation::ValidatedResult; +use colored::{ColoredString, Colorize}; +use std::path::{Path, PathBuf}; +use std::sync::{LazyLock, RwLock}; + +static LOG_STATE: LazyLock> = LazyLock::new(RwLock::default); + +struct LogState { + indent_count: usize, + indent_prefix: String, +} + +impl LogState { + pub fn new() -> Self { + Self { + indent_count: 0, + indent_prefix: "".to_string(), + } + } + + pub fn inc_indent(&mut self) { + self.indent_count += 1; + self.regen_indent_prefix() + } + + pub fn dec_indent(&mut self) { + self.indent_count -= 1; + self.regen_indent_prefix() + } + + fn regen_indent_prefix(&mut self) { + self.indent_prefix = " ".repeat(self.indent_count); + } +} + +impl Default for LogState { + fn default() -> Self { + Self::new() + } +} + +pub struct LogIndent; + +impl LogIndent { + pub fn new() -> Self { + LOG_STATE.write().unwrap().inc_indent(); + Self {} + } +} + +impl Default for LogIndent { + fn default() -> Self { + Self::new() + } +} + +impl Drop for LogIndent { + fn drop(&mut self) { + LOG_STATE.write().unwrap().dec_indent(); + } +} + +pub fn log_action>(action: &str, subject: T) { + println!( + "{}{} {}", + LOG_STATE.read().unwrap().indent_prefix, + action.log_color_action(), + subject.as_ref() + ) +} + +pub fn log_warn_action>(action: &str, subject: T) { + println!( + "{}{} {}", + LOG_STATE.read().unwrap().indent_prefix, + action.log_color_warn(), + subject.as_ref(), + ) +} + +pub fn log_skipping_up_to_date>(subject: T) { + log_warn_action( + "Skipping", + format!( + "{}, {}", + subject.as_ref(), + "UP-TO-DATE".log_color_ok_highlight() + ), + ); +} + +pub fn log_validated_action_result(action: &str, result: &ValidatedResult, to_log: F) +where + F: FnOnce(&T) -> String, +{ + if let Some(value) = result.as_ok_ref() { + log_action(action, to_log(value)) + } +} + +pub fn log_action_plan(action: &OverwriteSafeAction, plan: OverwriteSafeActionPlan) { + match plan { + OverwriteSafeActionPlan::Create => match action { + OverwriteSafeAction::CopyFile { source, target } => { + log_action( + "Copying", + format!( + "{} to {}", + source.log_color_highlight(), + target.log_color_highlight() + ), + ); + } + OverwriteSafeAction::CopyFileTransformed { source, target, .. } => { + log_action( + "Copying", + format!( + "{} to {} transformed", + source.log_color_highlight(), + target.log_color_highlight() + ), + ); + } + OverwriteSafeAction::WriteFile { target, .. } => { + log_action("Creating", format!("{}", target.log_color_highlight())); + } + }, + OverwriteSafeActionPlan::Overwrite => match action { + OverwriteSafeAction::CopyFile { source, target } => { + log_warn_action( + "Overwriting", + format!( + "{} with {}", + target.log_color_highlight(), + source.log_color_highlight() + ), + ); + } + OverwriteSafeAction::CopyFileTransformed { source, target, .. } => { + log_warn_action( + "Overwriting", + format!( + "{} with {} transformed", + target.log_color_highlight(), + source.log_color_highlight() + ), + ); + } + OverwriteSafeAction::WriteFile { content: _, target } => { + log_warn_action("Overwriting", format!("{}", target.log_color_highlight())); + } + }, + OverwriteSafeActionPlan::SkipSameContent => match action { + OverwriteSafeAction::CopyFile { source, target } => { + log_warn_action( + "Skipping", + format!( + "copying {} to {}, content already up-to-date", + source.log_color_highlight(), + target.log_color_highlight(), + ), + ); + } + OverwriteSafeAction::CopyFileTransformed { source, target, .. } => { + log_warn_action( + "Skipping", + format!( + "copying {} to {} transformed, content already up-to-date", + source.log_color_highlight(), + target.log_color_highlight() + ), + ); + } + OverwriteSafeAction::WriteFile { content: _, target } => { + log_warn_action( + "Skipping", + format!( + "generating {}, content already up-to-date", + target.log_color_highlight() + ), + ); + } + }, + } +} + +pub trait LogColorize { + fn as_str(&self) -> impl Colorize; + + fn log_color_action(&self) -> ColoredString { + self.as_str().green() + } + + fn log_color_warn(&self) -> ColoredString { + self.as_str().yellow() + } + + fn log_color_error(&self) -> ColoredString { + self.as_str().red() + } + + fn log_color_highlight(&self) -> ColoredString { + self.as_str().bold() + } + + fn log_color_error_highlight(&self) -> ColoredString { + self.as_str().bold().red().underline() + } + + fn log_color_ok_highlight(&self) -> ColoredString { + self.as_str().bold().green() + } +} + +impl<'a> LogColorize for &'a str { + fn as_str(&self) -> impl Colorize { + *self + } +} + +impl LogColorize for String { + fn as_str(&self) -> impl Colorize { + self.as_str() + } +} + +impl<'a> LogColorize for &'a Path { + fn as_str(&self) -> impl Colorize { + ColoredString::from(self.display().to_string()) + } +} + +impl LogColorize for PathBuf { + fn as_str(&self) -> impl Colorize { + ColoredString::from(self.display().to_string()) + } +} + +impl> LogColorize for PathExtra

{ + fn as_str(&self) -> impl Colorize { + ColoredString::from(self.display().to_string()) + } +} diff --git a/wasm-rpc-stubgen/src/main.rs b/wasm-rpc-stubgen/src/main.rs index df388c1d..7e6694ec 100644 --- a/wasm-rpc-stubgen/src/main.rs +++ b/wasm-rpc-stubgen/src/main.rs @@ -17,6 +17,9 @@ use colored::Colorize; use golem_wasm_rpc_stubgen::*; use std::process::ExitCode; +#[cfg(feature = "app-command")] +use golem_wasm_rpc_stubgen::model::app::ComponentPropertiesExtensionsNone; + #[tokio::main] async fn main() -> ExitCode { pretty_env_logger::init(); @@ -31,8 +34,10 @@ async fn main() -> ExitCode { Command::InitializeWorkspace(init_workspace_args) => { initialize_workspace(init_workspace_args, "wasm-rpc-stubgen", &[]) } - #[cfg(feature = "unstable-dec-dep")] - Command::App { subcommand } => run_declarative_command(subcommand).await, + #[cfg(feature = "app-command")] + Command::App { subcommand } => { + run_app_command::(subcommand).await + } }; match result { diff --git a/wasm-rpc-stubgen/src/make.rs b/wasm-rpc-stubgen/src/make.rs index 78381375..18b68304 100644 --- a/wasm-rpc-stubgen/src/make.rs +++ b/wasm-rpc-stubgen/src/make.rs @@ -12,10 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::commands::log::{log_action, log_warn_action}; -use crate::{cargo, GenerateArgs, WasmRpcOverride}; +use crate::log::{log_action, log_warn_action, LogColorize}; +use crate::{cargo, fs, GenerateArgs, WasmRpcOverride}; use heck::ToSnakeCase; -use std::fs; use std::path::Path; use std::process::Command; use toml::map::Map; @@ -443,7 +442,10 @@ pub fn initialize_workspace( let makefile = makefile.to_string()?; log_warn_action( "Overwriting", - format!("cargo-make Makefile {:?}", makefile_path), + format!( + "cargo-make Makefile {:?}", + makefile_path.log_color_highlight() + ), ); fs::write(makefile_path, makefile)?; } else if has_cargo_make() { @@ -459,7 +461,10 @@ pub fn initialize_workspace( let makefile = makefile.to_string()?; log_action( "Writing", - format!("cargo-make Makefile to {:?}", makefile_path), + format!( + "cargo-make Makefile to {:?}", + makefile_path.log_color_highlight() + ), ); fs::write(makefile_path, makefile)?; } else { @@ -470,7 +475,10 @@ pub fn initialize_workspace( let mut new_members = Vec::new(); for target in targets { - log_action("Generating", format!("initial stub for {target}")); + log_action( + "Generating", + format!("initial stub for {}", target.log_color_highlight()), + ); let stub_name = format!("{target}-stub"); crate::generate(GenerateArgs { diff --git a/wasm-rpc-stubgen/src/model/app.rs b/wasm-rpc-stubgen/src/model/app.rs new file mode 100644 index 00000000..e1dd1443 --- /dev/null +++ b/wasm-rpc-stubgen/src/model/app.rs @@ -0,0 +1,1163 @@ +use crate::log::LogColorize; +use crate::model::app_raw; +use crate::model::template::Template; +use crate::naming::wit::package_dep_dir_name_from_parser; +use crate::validation::{ValidatedResult, ValidationBuilder}; +use crate::{fs, naming}; +use heck::{ + ToKebabCase, ToLowerCamelCase, ToPascalCase, ToShoutyKebabCase, ToShoutySnakeCase, ToSnakeCase, + ToTitleCase, ToTrainCase, ToUpperCamelCase, +}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::fmt::Formatter; +use std::fmt::{Debug, Display}; +use std::hash::Hash; +use std::path::{Path, PathBuf}; +use wit_parser::PackageName; + +pub const DEFAULT_CONFIG_FILE_NAME: &str = "golem.yaml"; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ComponentName(String); + +impl ComponentName { + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl Display for ComponentName { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + +impl From for ComponentName { + fn from(value: String) -> Self { + ComponentName(value) + } +} + +impl From<&str> for ComponentName { + fn from(value: &str) -> Self { + Self(value.to_string()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ProfileName(String); + +impl ProfileName { + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl Display for ProfileName { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + +impl From for ProfileName { + fn from(value: String) -> Self { + ProfileName(value) + } +} + +impl From<&str> for ProfileName { + fn from(value: &str) -> Self { + Self(value.to_string()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct TemplateName(String); + +impl TemplateName { + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl Display for TemplateName { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + +impl From for TemplateName { + fn from(value: String) -> Self { + TemplateName(value) + } +} + +impl From<&str> for TemplateName { + fn from(value: &str) -> Self { + Self(value.to_string()) + } +} + +pub fn includes_from_yaml_file(source: &Path) -> Vec { + fs::read_to_string(source) + .ok() + .and_then(|source| app_raw::Application::from_yaml_str(source.as_str()).ok()) + .map(|app| { + if app.includes.is_empty() { + vec!["**/golem.yaml".to_string()] + } else { + app.includes + } + }) + .unwrap_or_default() +} + +#[derive(Clone, Debug)] +pub enum ResolvedComponentProperties { + Properties { + template_name: Option, + any_template_overrides: bool, + properties: ComponentProperties, + }, + Profiles { + template_name: Option, + any_template_overrides: HashMap, + default_profile: ProfileName, + profiles: HashMap>, + }, +} + +pub struct ComponentEffectivePropertySource<'a> { + pub template_name: Option<&'a TemplateName>, + pub profile: Option<&'a ProfileName>, + pub is_requested_profile: bool, + pub any_template_overrides: bool, +} + +#[derive(Clone, Debug)] +pub struct Application { + temp_dir: Option, + wit_deps: Vec, + components: BTreeMap>, + dependencies: BTreeMap>, + no_dependencies: BTreeSet, +} + +impl Application { + pub fn from_raw_apps(apps: Vec) -> ValidatedResult { + let mut validation = ValidationBuilder::new(); + + let mut include = Vec::::new(); + let mut include_sources = Vec::::new(); + + let mut temp_dir: Option = None; + let mut temp_dir_sources = Vec::::new(); + + let mut wit_deps = Vec::::new(); + let mut wit_deps_sources = Vec::::new(); + + let mut templates = HashMap::::new(); + let mut template_sources = HashMap::>::new(); + + let mut dependencies = BTreeMap::>::new(); + let mut dependency_sources = + HashMap::>>::new(); + + let mut components = HashMap::::new(); + let mut component_sources = HashMap::>::new(); + + for app in apps { + validation.push_context("source", app.source.to_string_lossy().to_string()); + + if let Some(dir) = app.application.temp_dir { + temp_dir_sources.push(app.source.to_path_buf()); + if temp_dir.is_none() { + temp_dir = Some(dir); + } + } + + if !app.application.includes.is_empty() { + include_sources.push(app.source.to_path_buf()); + if include.is_empty() { + include = app.application.includes; + } + } + + if !app.application.wit_deps.is_empty() { + wit_deps_sources.push(app.source.to_path_buf()); + if wit_deps.is_empty() { + wit_deps = app.application.wit_deps; // TODO: resolve from source? + } + } + + for (template_name, template) in app.application.templates { + validation.push_context("template", template_name.clone()); + + let mut invalid_template = false; + if template.profiles.is_empty() { + if template.default_profile.is_some() { + validation.add_error(format!( + "When {} is not defined then {} should not be defined", + "profiles".log_color_highlight(), + "defaultProfile".log_color_highlight() + )); + invalid_template = true; + } + } else { + let defined_property_names = + template.component_properties.defined_property_names(); + if !defined_property_names.is_empty() { + for property_name in defined_property_names { + validation.add_error(format!( + "When {} is defined then {} should not be defined", + "profiles".log_color_highlight(), + property_name.log_color_highlight() + )); + invalid_template = true; + } + } + + if template.default_profile.is_none() { + validation.add_error(format!( + "When {} is defined then {} is mandatory", + "profiles".log_color_highlight(), + "defaultProfile".log_color_highlight() + )); + invalid_template = true; + } + } + + let template_name = TemplateName::from(template_name); + if template_sources.contains_key(&template_name) { + template_sources + .get_mut(&template_name) + .unwrap() + .push(app.source.to_path_buf()); + } else { + template_sources.insert(template_name.clone(), vec![app.source.to_path_buf()]); + } + if !templates.contains_key(&template_name) && !invalid_template { + templates.insert(template_name, template); + } + + validation.pop_context(); + } + + for (component_name, component) in app.application.components { + let component_name = ComponentName::from(component_name); + + if !component_sources.contains_key(&component_name) { + component_sources.insert(component_name.clone(), Vec::new()); + } + component_sources + .get_mut(&component_name) + .unwrap() + .push(app.source.to_path_buf()); + + components.insert(component_name, (app.source.to_path_buf(), component)); + } + + for (component_name, component_dependencies) in app.application.dependencies { + let component_name = ComponentName::from(component_name); + validation.push_context("component", component_name.to_string()); + + for dependency in component_dependencies { + if dependency.type_ == "wasm-rpc" { + match dependency.target { + Some(target) => { + let target_component_name = ComponentName::from(target); + + if !dependencies.contains_key(&component_name) { + dependencies.insert(component_name.clone(), BTreeSet::new()); + } + dependencies + .get_mut(&component_name) + .unwrap() + .insert(target_component_name.clone()); + + if !dependency_sources.contains_key(&component_name) { + dependency_sources + .insert(component_name.clone(), HashMap::new()); + } + let dependency_sources = + dependency_sources.get_mut(&component_name).unwrap(); + if !dependency_sources.contains_key(&target_component_name) { + dependency_sources + .insert(target_component_name.clone(), Vec::new()); + } + dependency_sources + .get_mut(&target_component_name) + .unwrap() + .push(app.source.to_path_buf()); + } + None => validation.add_error(format!( + "Missing {} field for component wasm-rpc dependency", + "target".log_color_error_highlight() + )), + } + } else { + validation.add_error(format!( + "Unknown component dependency type: {}", + dependency.type_.log_color_error_highlight() + )); + } + } + + validation.pop_context(); + } + + validation.pop_context(); + } + + for (property_name, sources) in [ + ("include", include_sources), + ("tempDir", temp_dir_sources), + ("witDeps", wit_deps_sources), + ] { + if sources.len() > 1 { + validation.add_error(format!( + "Property {} is defined in multiple sources: {}", + property_name.log_color_highlight(), + sources + .into_iter() + .map(|s| s.log_color_highlight()) + .join(", ") + )) + } + } + + let non_unique_templates = template_sources + .into_iter() + .filter(|(_, sources)| sources.len() > 1); + + validation.add_errors(non_unique_templates, |(template_name, sources)| { + Some(( + vec![], + format!( + "Template {} defined multiple times in sources: {}", + template_name.as_str().log_color_highlight(), + sources + .into_iter() + .map(|s| s.log_color_highlight()) + .join(", ") + ), + )) + }); + + let non_unique_components = component_sources + .into_iter() + .filter(|(_, sources)| sources.len() > 1); + + validation.add_errors(non_unique_components, |(template_name, sources)| { + Some(( + vec![], + format!( + "Component {} defined multiple times in sources: {}", + template_name.as_str().log_color_highlight(), + sources + .into_iter() + .map(|s| s.log_color_highlight()) + .join(", ") + ), + )) + }); + + for (component_name, dependency_sources) in dependency_sources { + for (target_component_name, dependency_sources) in dependency_sources { + if dependency_sources.len() > 1 { + validation.push_context("component", component_name.to_string()); + validation.push_context("target", target_component_name.to_string()); + + validation.add_warn(format!( + "WASM-RPC dependency is defined multiple times, sources: {}", + dependency_sources + .into_iter() + .map(|s| s.log_color_highlight()) + .join(", ") + )); + + validation.pop_context(); + validation.pop_context(); + } + } + } + + let components = { + let template_env = Self::template_env(); + + let mut resolved_components = BTreeMap::>::new(); + for (component_name, (source, mut component)) in components { + validation.push_context("source", source.to_string_lossy().to_string()); + validation.push_context("component", component_name.to_string()); + + let template_with_name = match component.template { + Some(template_name) => { + let template_name = TemplateName::from(template_name); + match templates.get(&template_name) { + Some(template) => Some(Some((template_name, template))), + None => { + validation.add_error(format!( + "Component references unknown template: {}", + template_name.as_str().log_color_error_highlight() + )); + None + } + } + } + None => Some(None), + }; + + if let Some(template_with_name) = template_with_name { + let component_properties = match template_with_name { + Some((template_name, template)) => { + let mut incompatible_overrides = false; + + let defined_property_names = + component.component_properties.defined_property_names(); + + if !template.profiles.is_empty() && !defined_property_names.is_empty() { + incompatible_overrides = true; + for property_name in defined_property_names { + validation.add_error( + format!( + "Property {} cannot be used, as the component uses a template with profiles", + property_name.log_color_highlight() + ) + ); + } + } + + for profile_name in component.profiles.keys() { + if !template.profiles.contains_key(profile_name) { + incompatible_overrides = true; + validation.add_error( + format!( + "Profile {} cannot be used, as the component uses template {} with the following profiles: {}", + profile_name.log_color_highlight(), + template_name.as_str().log_color_highlight(), + component.profiles.keys().map(|s| s.log_color_highlight()).join(", ") + ) + ); + } + } + + if incompatible_overrides { + None + } else { + let template_context = minijinja::context! { componentName => component_name.as_str() }; + + if template.profiles.is_empty() { + let rendered_template_properties = + ComponentProperties::from_raw_template( + &template_env, + &template_context, + &template.component_properties, + ); + + match rendered_template_properties { + Ok(rendered_template_properties) => { + let (properties, any_template_overrides) = + rendered_template_properties.merge_with_overrides( + component.component_properties, + ); + Some(ResolvedComponentProperties::Properties { + template_name: Some(template_name), + any_template_overrides, + properties, + }) + } + Err(err) => { + validation.add_error(format!( + "Failed to render template {}, error: {}", + template_name.as_str().log_color_highlight(), + err.to_string().log_color_error_highlight() + )); + None + } + } + } else { + let mut any_template_overrides = + HashMap::::new(); + let mut profiles = + HashMap::>::new(); + let mut any_template_render_error = false; + + for (profile_name, template_component_properties) in + &template.profiles + { + let rendered_template_properties = + ComponentProperties::from_raw_template( + &template_env, + &template_context, + template_component_properties, + ); + match rendered_template_properties { + Ok(rendered_template_properties) => { + let (properties, any_overrides) = { + if let Some(component_properties) = + component.profiles.remove(profile_name) + { + rendered_template_properties + .merge_with_overrides( + component_properties, + ) + } else { + (rendered_template_properties, false) + } + }; + + any_template_overrides.insert( + profile_name.clone().into(), + any_overrides, + ); + profiles.insert( + profile_name.clone().into(), + properties, + ); + } + Err(err) => { + validation.add_error(format!( + "Failed to render template {}, error: {}", + template_name.as_str().log_color_highlight(), + err.to_string().log_color_error_highlight() + )); + any_template_render_error = true + } + } + } + + (!any_template_render_error).then(|| { + ResolvedComponentProperties::Profiles { + template_name: Some(template_name), + any_template_overrides, + default_profile: template + .default_profile + .clone() + .expect("Missing template default profile") + .into(), + profiles, + } + }) + } + } + } + None => { + if component.profiles.is_empty() { + if component.default_profile.is_some() { + validation.add_error(format!( + "When {} is not defined then {} should not be defined", + "profiles".log_color_highlight(), + "defaultProfile".log_color_highlight() + )); + None + } else { + let properties = ComponentProperties::::from_raw( + component.component_properties, + ); + + match properties { + Ok(properties) => { + Some(ResolvedComponentProperties::Properties { + template_name: None, + any_template_overrides: false, + properties, + }) + } + Err(err) => { + validation.add_error(format!("{:?}", err)); + None + } + } + } + } else if component.default_profile.is_none() { + validation.add_error(format!( + "When {} is defined then {} is mandatory", + "profiles".log_color_highlight(), + "defaultProfile".log_color_highlight() + )); + None + } else { + Some(ResolvedComponentProperties::Profiles { + template_name: None, + any_template_overrides: component + .profiles + .keys() + .map(|profile_name| { + (ProfileName::from(profile_name.clone()), false) + }) + .collect(), + default_profile: component.default_profile.unwrap().into(), + profiles: component + .profiles + .into_iter() + .filter_map(|(profile_name, properties)| { + match ComponentProperties::::from_raw(properties) { + Ok(properties) => Some(( + ProfileName::from(profile_name), + properties, + )), + Err(err) => { + validation.add_error(format!("{:?}", err)); + None + } + } + }) + .collect(), + }) + } + } + }; + + if let Some(mut properties) = component_properties { + fn validate_properties_and_convert_extensions< + CPE: ComponentPropertiesExtensions, + >( + source: &Path, + validation: &mut ValidationBuilder, + properties: &mut ComponentProperties, + ) -> bool { + let mut any_error = false; + + for (name, value) in [ + ("sourceWit", &properties.source_wit), + ("generatedWit", &properties.source_wit), + ("componentWasm", &properties.source_wit), + ] { + if value.is_empty() { + validation.add_error(format!( + "Property {} is empty or undefined", + name.log_color_highlight() + )); + any_error = true; + } + } + + properties.extensions = CPE::convert_and_validate( + source, + validation, + properties.extensions_raw.take().unwrap(), + ); + any_error |= properties.extensions.is_none(); + + any_error + } + + let any_error: bool = match &mut properties { + ResolvedComponentProperties::Properties { + template_name, + any_template_overrides, + properties, + } => { + template_name.iter().for_each(|template_name| { + validation.push_context("template", template_name.to_string()); + validation.push_context( + "overrides", + any_template_overrides.to_string(), + ); + }); + + let any_error = validate_properties_and_convert_extensions( + &source, + &mut validation, + properties, + ); + + if template_name.is_some() { + validation.pop_context(); + validation.pop_context(); + } + + any_error + } + ResolvedComponentProperties::Profiles { + template_name, + any_template_overrides, + profiles, + .. + } => { + template_name.iter().for_each(|template_name| { + validation.push_context("template", template_name.to_string()); + }); + + let mut any_error = false; + + for (profile_name, properties) in profiles { + validation.push_context("profile", profile_name.to_string()); + let any_template_overrides = + any_template_overrides.get(profile_name); + any_template_overrides.iter().for_each( + |any_template_overrides| { + validation.push_context( + "overrides", + any_template_overrides.to_string(), + ); + }, + ); + + any_error |= validate_properties_and_convert_extensions( + &source, + &mut validation, + properties, + ); + + if any_template_overrides.is_some() { + validation.pop_context(); + } + validation.pop_context(); + } + + if template_name.is_some() { + validation.pop_context(); + } + + any_error + } + }; + + if !any_error { + resolved_components + .insert(component_name.clone(), Component { source, properties }); + } + } + } + + validation.pop_context(); + validation.pop_context(); + } + + resolved_components + }; + + validation.build(Self { + temp_dir, + wit_deps, + components, + dependencies, + no_dependencies: BTreeSet::new(), + }) + } + + fn template_env<'a>() -> minijinja::Environment<'a> { + let mut env = minijinja::Environment::new(); + + env.add_filter("to_snake_case", |str: &str| str.to_snake_case()); + + env.add_filter("to_kebab_case", |str: &str| str.to_kebab_case()); + env.add_filter("to_lower_camel_case", |str: &str| str.to_lower_camel_case()); + env.add_filter("to_pascal_case", |str: &str| str.to_pascal_case()); + env.add_filter("to_shouty_kebab_case", |str: &str| { + str.to_shouty_kebab_case() + }); + env.add_filter("to_shouty_snake_case", |str: &str| { + str.to_shouty_snake_case() + }); + env.add_filter("to_snake_case", |str: &str| str.to_snake_case()); + env.add_filter("to_title_case", |str: &str| str.to_title_case()); + env.add_filter("to_train_case", |str: &str| str.to_train_case()); + env.add_filter("to_upper_camel_case", |str: &str| str.to_upper_camel_case()); + + env + } + + pub fn component_names(&self) -> impl Iterator { + self.components.keys() + } + + pub fn wit_deps(&self) -> Vec { + self.wit_deps.iter().map(PathBuf::from).collect() + } + + pub fn all_wasm_rpc_dependencies(&self) -> BTreeSet { + self.dependencies.values().flatten().cloned().collect() + } + + pub fn all_profiles(&self) -> BTreeSet { + self.component_names() + .flat_map(|component_name| self.component_profiles(component_name)) + .collect() + } + + pub fn all_option_profiles(&self) -> BTreeSet> { + let mut profiles = self + .component_names() + .flat_map(|component_name| self.component_profiles(component_name)) + .map(Some) + .collect::>(); + profiles.insert(None); + profiles + } + + pub fn all_custom_commands(&self, profile: Option<&ProfileName>) -> BTreeSet { + self.component_names() + .flat_map(|component_name| { + self.component_properties(component_name, profile) + .custom_commands + .keys() + .cloned() + }) + .collect() + } + + pub fn temp_dir(&self) -> &Path { + match self.temp_dir.as_ref() { + Some(temp_dir) => Path::new(temp_dir), + None => Path::new("golem-temp"), + } + } + + fn component(&self, component_name: &ComponentName) -> &Component { + self.components + .get(component_name) + .unwrap_or_else(|| panic!("Component not found: {}", component_name)) + } + + pub fn component_source_dir(&self, component_name: &ComponentName) -> &Path { + self.component(component_name).source_dir() + } + + pub fn component_wasm_rpc_dependencies( + &self, + component_name: &ComponentName, + ) -> &BTreeSet { + self.dependencies + .get(component_name) + .unwrap_or(&self.no_dependencies) + } + + fn component_profiles(&self, component_name: &ComponentName) -> HashSet { + match &self.component(component_name).properties { + ResolvedComponentProperties::Properties { .. } => HashSet::new(), + ResolvedComponentProperties::Profiles { profiles, .. } => { + profiles.keys().cloned().collect() + } + } + } + + pub fn component_effective_property_source<'a>( + &'a self, + component_name: &ComponentName, + profile: Option<&'a ProfileName>, + ) -> ComponentEffectivePropertySource<'a> { + match &self.component(component_name).properties { + ResolvedComponentProperties::Properties { + template_name, + any_template_overrides, + .. + } => ComponentEffectivePropertySource { + template_name: template_name.as_ref(), + profile: None, + is_requested_profile: false, + any_template_overrides: *any_template_overrides, + }, + ResolvedComponentProperties::Profiles { + template_name, + any_template_overrides, + default_profile, + profiles, + } => { + let effective_profile = profile + .map(|profile| { + if profiles.contains_key(profile) { + profile + } else { + default_profile + } + }) + .unwrap_or_else(|| default_profile); + + let is_requested_profile = Some(&effective_profile) == profile.as_ref(); + + let any_template_overrides = any_template_overrides + .get(effective_profile) + .cloned() + .unwrap_or_default(); + ComponentEffectivePropertySource { + template_name: template_name.as_ref(), + profile, + is_requested_profile, + any_template_overrides, + } + } + } + } + + pub fn component_properties( + &self, + component_name: &ComponentName, + profile: Option<&ProfileName>, + ) -> &ComponentProperties { + match &self.component(component_name).properties { + ResolvedComponentProperties::Properties { properties, .. } => properties, + ResolvedComponentProperties::Profiles { + default_profile, + profiles, + .. + } => profiles + .get( + profile + .map(|profile| { + if profiles.contains_key(profile) { + profile + } else { + default_profile + } + }) + .unwrap_or_else(|| default_profile), + ) + .unwrap(), + } + } + + pub fn component_source_wit( + &self, + component_name: &ComponentName, + profile: Option<&ProfileName>, + ) -> PathBuf { + let component = self.component(component_name); + component.source_dir().join( + self.component_properties(component_name, profile) + .source_wit + .clone(), + ) + } + + pub fn component_generated_base_wit(&self, component_name: &ComponentName) -> PathBuf { + self.temp_dir() + .join("generated-base-wit") + .join(component_name.as_str()) + } + + pub fn component_generated_base_wit_interface_package_dir( + &self, + component_name: &ComponentName, + interface_package_name: &PackageName, + ) -> PathBuf { + self.component_generated_base_wit(component_name) + .join(naming::wit::DEPS_DIR) + .join(package_dep_dir_name_from_parser(interface_package_name)) + .join(naming::wit::INTERFACE_WIT_FILE_NAME) + } + + pub fn component_generated_wit( + &self, + component_name: &ComponentName, + profile: Option<&ProfileName>, + ) -> PathBuf { + let component = self.component(component_name); + component.source_dir().join( + self.component_properties(component_name, profile) + .generated_wit + .clone(), + ) + } + + pub fn component_wasm( + &self, + component_name: &ComponentName, + profile: Option<&ProfileName>, + ) -> PathBuf { + let component = self.component(component_name); + component.source_dir().join( + self.component_properties(component_name, profile) + .component_wasm + .clone(), + ) + } + + pub fn component_linked_wasm( + &self, + component_name: &ComponentName, + profile: Option<&ProfileName>, + ) -> PathBuf { + self.component_source_dir(component_name).join( + self.component_properties(component_name, profile) + .linked_wasm + .as_ref() + .cloned() + .map(PathBuf::from) + .unwrap_or_else(|| { + self.temp_dir() + .join("linked-wasm") + .join(format!("{}.wasm", component_name.as_str())) + }), + ) + } + + fn stub_build_dir(&self) -> PathBuf { + self.temp_dir().join("stub") + } + + pub fn stub_temp_build_dir(&self, component_name: &ComponentName) -> PathBuf { + self.stub_build_dir() + .join(component_name.as_str()) + .join("temp-build") + } + + pub fn stub_wasm(&self, component_name: &ComponentName) -> PathBuf { + self.stub_build_dir() + .join(component_name.as_str()) + .join("stub.wasm") + } + + pub fn stub_wit(&self, component_name: &ComponentName) -> PathBuf { + self.stub_build_dir() + .join(component_name.as_str()) + .join(naming::wit::WIT_DIR) + } +} + +#[derive(Clone, Debug)] +pub struct Component { + pub source: PathBuf, + pub properties: ResolvedComponentProperties, +} + +impl Component { + pub fn source_dir(&self) -> &Path { + self.source.parent().unwrap_or_else(|| { + panic!( + "Failed to get parent for component, source: {}", + self.source.display() + ) + }) + } +} + +#[derive(Clone, Debug)] +pub struct ComponentProperties { + pub source_wit: String, + pub generated_wit: String, + pub component_wasm: String, + pub linked_wasm: Option, + pub build: Vec, + pub custom_commands: HashMap>, + pub clean: Vec, + + // TODO: clean up: move extensions_raw to a temporary var and make extensions non optional + pub extensions_raw: Option, + pub extensions: Option, +} + +impl ComponentProperties { + fn from_raw(raw: app_raw::ComponentProperties) -> anyhow::Result { + Ok(Self { + source_wit: raw.source_wit.unwrap_or_default(), + generated_wit: raw.generated_wit.unwrap_or_default(), + component_wasm: raw.component_wasm.unwrap_or_default(), + linked_wasm: raw.linked_wasm, + build: raw.build, + custom_commands: raw.custom_commands, + clean: raw.clean, + extensions_raw: Some(CPE::raw_from_serde_json(serde_json::Value::Object( + raw.extensions, + ))?), + extensions: None, + }) + } + + fn from_raw_template( + env: &minijinja::Environment, + ctx: &C, + template_properties: &app_raw::ComponentProperties, + ) -> anyhow::Result { + ComponentProperties::from_raw(template_properties.render(env, ctx)?) + } + + fn merge_with_overrides(mut self, overrides: app_raw::ComponentProperties) -> (Self, bool) { + let mut any_overrides = false; + + if let Some(source_wit) = overrides.source_wit { + self.source_wit = source_wit; + any_overrides = true; + } + + if let Some(generated_wit) = overrides.generated_wit { + self.generated_wit = generated_wit; + any_overrides = true; + } + + if let Some(component_wasm) = overrides.component_wasm { + self.component_wasm = component_wasm; + any_overrides = true; + } + + if overrides.linked_wasm.is_some() { + self.linked_wasm = overrides.linked_wasm; + any_overrides = true; + } + + if !overrides.build.is_empty() { + self.build = overrides.build; + any_overrides = true; + } + + for (custom_command_name, custom_command) in overrides.custom_commands { + if self.custom_commands.contains_key(&custom_command_name) { + any_overrides = true; + } + self.custom_commands + .insert(custom_command_name, custom_command); + } + + (self, any_overrides) + } +} + +pub trait ComponentPropertiesExtensions: Sized + Debug + Clone { + type Raw: Debug + Clone; + + fn raw_from_serde_json(extensions: serde_json::Value) -> serde_json::Result; + + fn convert_and_validate( + source: &Path, + validation: &mut ValidationBuilder, + raw: Self::Raw, + ) -> Option; +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ComponentPropertiesExtensionsNone {} + +impl ComponentPropertiesExtensions for ComponentPropertiesExtensionsNone { + type Raw = Self; + + fn raw_from_serde_json(extensions: serde_json::Value) -> serde_json::Result + where + Self: Sized, + { + serde_json::from_value(extensions) + } + + fn convert_and_validate( + _source: &Path, + _validation: &mut ValidationBuilder, + raw: Self::Raw, + ) -> Option { + Some(raw) + } +} + +#[derive(Debug, Clone)] +pub struct ComponentPropertiesExtensionsAny; + +impl ComponentPropertiesExtensions for ComponentPropertiesExtensionsAny { + type Raw = Self; + + fn raw_from_serde_json(_extensions: serde_json::Value) -> serde_json::Result + where + Self: Sized, + { + Ok(ComponentPropertiesExtensionsAny) + } + + fn convert_and_validate( + _source: &Path, + _validation: &mut ValidationBuilder, + raw: Self::Raw, + ) -> Option { + Some(raw) + } +} diff --git a/wasm-rpc-stubgen/src/model/app_raw.rs b/wasm-rpc-stubgen/src/model/app_raw.rs new file mode 100644 index 00000000..bdffbf72 --- /dev/null +++ b/wasm-rpc-stubgen/src/model/app_raw.rs @@ -0,0 +1,156 @@ +use crate::fs; +use crate::log::LogColorize; +use anyhow::{anyhow, Context}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::PathBuf; + +#[derive(Clone, Debug)] +pub struct ApplicationWithSource { + pub source: PathBuf, + pub application: Application, +} + +impl ApplicationWithSource { + pub fn from_yaml_file(file: PathBuf) -> anyhow::Result { + Self::from_yaml_string(file.clone(), fs::read_to_string(file.clone())?) + .with_context(|| anyhow!("Failed to load source {}", file.log_color_highlight())) + } + + pub fn from_yaml_string(source: PathBuf, string: String) -> serde_yaml::Result { + Ok(Self { + source, + application: Application::from_yaml_str(string.as_str())?, + }) + } + + pub fn source_as_string(&self) -> String { + self.source.to_string_lossy().to_string() + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Application { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub includes: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub temp_dir: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub wit_deps: Vec, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub templates: HashMap, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub components: HashMap, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub dependencies: HashMap>, +} + +impl Application { + pub fn from_yaml_str(yaml: &str) -> serde_yaml::Result { + serde_yaml::from_str(yaml) + } + + pub fn to_yaml_string(&self) -> String { + serde_yaml::to_string(self).expect("Failed to serialize Application as YAML") + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ComponentTemplate { + #[serde(flatten)] + pub component_properties: ComponentProperties, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub profiles: HashMap, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub default_profile: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Component { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub template: Option, + #[serde(flatten)] + pub component_properties: ComponentProperties, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub profiles: HashMap, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub default_profile: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ComponentProperties { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub source_wit: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub generated_wit: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub component_wasm: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub linked_wasm: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub build: Vec, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub custom_commands: HashMap>, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub clean: Vec, + #[serde(flatten)] + pub extensions: serde_json::Map, +} + +impl ComponentProperties { + pub fn defined_property_names(&self) -> Vec<&str> { + let mut vec = Vec::<&str>::new(); + + if self.source_wit.is_some() { + vec.push("sourceWit"); + } + + if self.generated_wit.is_some() { + vec.push("generatedWit"); + } + + if self.component_wasm.is_some() { + vec.push("componentWasm"); + } + + if self.linked_wasm.is_some() { + vec.push("linkedWasm"); + } + + if !self.build.is_empty() { + vec.push("build"); + } + + if !self.custom_commands.is_empty() { + vec.push("customCommands"); + } + + self.extensions.keys().for_each(|name| vec.push(name)); + + vec + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ExternalCommand { + pub command: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub dir: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub sources: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub targets: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Dependency { + #[serde(rename = "type")] + pub type_: String, + pub target: Option, +} diff --git a/wasm-rpc-stubgen/src/model/mod.rs b/wasm-rpc-stubgen/src/model/mod.rs index fdaddf47..3b59e2e5 100644 --- a/wasm-rpc-stubgen/src/model/mod.rs +++ b/wasm-rpc-stubgen/src/model/mod.rs @@ -1,4 +1,3 @@ -pub mod oam; -pub mod unknown_properties; -pub mod validation; -pub mod wasm_rpc; +pub mod app; +pub mod app_raw; +pub mod template; diff --git a/wasm-rpc-stubgen/src/model/oam.rs b/wasm-rpc-stubgen/src/model/oam.rs deleted file mode 100644 index 037946c8..00000000 --- a/wasm-rpc-stubgen/src/model/oam.rs +++ /dev/null @@ -1,298 +0,0 @@ -use crate::model::validation::{ValidatedResult, ValidationBuilder}; -use anyhow::Context; -use itertools::Itertools; -use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; -use std::collections::{BTreeMap, BTreeSet}; -use std::path::PathBuf; - -pub const API_VERSION_V1BETA1: &str = "core.oam.dev/v1beta1"; -pub const KIND_APPLICATION: &str = "Application"; - -#[derive(Clone, Debug)] -pub struct ApplicationWithSource { - pub source: PathBuf, - pub application: Application, -} - -impl ApplicationWithSource { - pub fn from_yaml_file(file: PathBuf) -> anyhow::Result { - let content = std::fs::read_to_string(file.as_path()) - .with_context(|| format!("Failed to load file: {}", file.to_string_lossy()))?; - - Ok(Self::from_yaml_string(file, content)?) - } - - pub fn from_yaml_string(source: PathBuf, string: String) -> serde_yaml::Result { - Ok(Self { - source, - application: Application::from_yaml_str(string.as_str())?, - }) - } - - pub fn source_as_string(&self) -> String { - self.source.to_string_lossy().to_string() - } - - // NOTE: unlike the wasm_rpc model, here validation is optional separate step, so we can access the "raw" data - pub fn validate(self) -> ValidatedResult { - let mut validation = ValidationBuilder::new(); - validation.push_context("source", self.source_as_string()); - - if self.application.api_version != API_VERSION_V1BETA1 { - validation.add_warn(format!("Expected apiVersion: {}", API_VERSION_V1BETA1)) - } - - if self.application.kind != KIND_APPLICATION { - validation.add_error(format!("Expected kind: {}", KIND_APPLICATION)) - } - - self.application - .spec - .components - .iter() - .map(|component| &component.name) - .counts() - .into_iter() - .filter(|(_, count)| *count > 1) - .for_each(|(component_name, count)| { - validation.add_warn(format!( - "Component specified multiple times component: {}, count: {}", - component_name, count - )); - }); - - validation.pop_context(); - validation.build(self) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Application { - #[serde(rename = "apiVersion")] - pub api_version: String, - pub kind: String, - pub metadata: Metadata, - pub spec: Spec, -} - -impl Application { - pub fn new(name: String) -> Self { - Self { - api_version: API_VERSION_V1BETA1.to_string(), - kind: KIND_APPLICATION.to_string(), - metadata: Metadata { - name, - annotations: Default::default(), - labels: Default::default(), - }, - spec: Spec { components: vec![] }, - } - } - - pub fn from_yaml_str(yaml: &str) -> serde_yaml::Result { - serde_yaml::from_str(yaml) - } - - pub fn to_yaml_string(&self) -> String { - serde_yaml::to_string(self).expect("Failed to serialize Application as YAML") - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Metadata { - pub name: String, - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub annotations: BTreeMap, - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub labels: BTreeMap, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Spec { - pub components: Vec, -} - -impl Spec { - pub fn extract_components_by_type( - &mut self, - component_types: &BTreeSet<&'static str>, - ) -> BTreeMap<&'static str, Vec> { - let mut components = Vec::::new(); - - std::mem::swap(&mut components, &mut self.components); - - let mut matching_components = BTreeMap::<&'static str, Vec>::new(); - let mut remaining_components = Vec::::new(); - - for component in components { - if let Some(component_type) = component_types.get(component.component_type.as_str()) { - matching_components - .entry(component_type) - .or_default() - .push(component) - } else { - remaining_components.push(component) - } - } - - std::mem::swap(&mut remaining_components, &mut self.components); - - matching_components - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Component { - pub name: String, - #[serde(rename = "type")] - pub component_type: String, - pub properties: serde_json::Value, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub traits: Vec, -} - -pub trait TypedComponentProperties: Serialize + DeserializeOwned { - fn component_type() -> &'static str; -} - -impl Component { - pub fn typed_properties(&self) -> Result { - if self.component_type != T::component_type() { - panic!( - "Component type mismatch in clone_properties_as, self: {}, requested: {}", - self.component_type, - T::component_type() - ); - } - serde_json::from_value(self.properties.clone()) - } - - pub fn set_typed_properties(&mut self, properties: T) { - self.component_type = T::component_type().to_string(); - self.properties = serde_json::to_value(properties).expect("Failed to serialize properties"); - } - - pub fn extract_traits_by_type( - &mut self, - trait_types: &BTreeSet<&'static str>, - ) -> BTreeMap<&'static str, Vec> { - let mut component_traits = Vec::::new(); - - std::mem::swap(&mut component_traits, &mut self.traits); - - let mut matching_traits = BTreeMap::<&'static str, Vec>::new(); - let mut remaining_traits = Vec::::new(); - - for component_trait in component_traits { - if let Some(trait_type) = trait_types.get(component_trait.trait_type.as_str()) { - matching_traits - .entry(trait_type) - .or_default() - .push(component_trait); - } else { - remaining_traits.push(component_trait); - } - } - - std::mem::swap(&mut remaining_traits, &mut self.traits); - - matching_traits - } - - pub fn add_typed_trait(&mut self, properties: T) { - self.traits.push(Trait { - trait_type: T::trait_type().to_string(), - properties: serde_json::to_value(properties).expect("Failed to serialize typed trait"), - }); - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Trait { - #[serde(rename = "type")] - pub trait_type: String, - pub properties: serde_json::Value, -} - -pub trait TypedTraitProperties: Serialize + DeserializeOwned { - fn trait_type() -> &'static str; - - fn from_generic_trait(value: Trait) -> Result { - if value.trait_type != Self::trait_type() { - panic!( - "Trait type mismatch in TryFrom, value: {}, typed: {}", - value.trait_type, - Self::trait_type() - ) - } - serde_json::from_value(value.properties) - } -} - -#[cfg(test)] -mod tests { - use test_r::test; - - use super::*; - use assert2::assert; - - #[test] - fn deserialize_example_application() { - let application: Application = serde_yaml::from_str( - r#" -apiVersion: core.oam.dev/v1beta1 -metadata: - name: "App name" -kind: Application -spec: - components: - - name: component-one - type: test-component-type - properties: - testComponentProperty: aaa - traits: - - type: test-trait-type-1 - properties: - testProperty: bbb - - type: test-trait-type-2 - properties: - testTraitProperty: ccc -"#, - ) - .unwrap(); - - assert!(application.api_version == API_VERSION_V1BETA1); - assert!(application.kind == KIND_APPLICATION); - assert!(application.metadata.name == "App name"); - assert!(application.spec.components.len() == 1); - - let component = &application.spec.components[0]; - - assert!(component.name == "component-one"); - assert!(component.component_type == "test-component-type"); - assert!(component.properties.is_object()); - - let properties = component.properties.as_object().unwrap(); - - assert!( - properties - .get_key_value("testComponentProperty") - .unwrap() - .1 - .as_str() - == Some("aaa") - ); - - assert!(component.traits.len() == 2); - - let component_trait = &component.traits[1]; - - assert!(component_trait.trait_type == "test-trait-type-2"); - assert!(component_trait.properties.is_object()); - - let properties = component_trait.properties.as_object().unwrap(); - - assert!(properties.get_key_value("testTraitProperty").unwrap().1 == "ccc"); - } -} diff --git a/wasm-rpc-stubgen/src/model/template.rs b/wasm-rpc-stubgen/src/model/template.rs new file mode 100644 index 00000000..3dc7741b --- /dev/null +++ b/wasm-rpc-stubgen/src/model/template.rs @@ -0,0 +1,155 @@ +use crate::model::app_raw; +use serde::Serialize; +use std::collections::HashMap; + +pub trait Template { + type Rendered; + + fn render( + &self, + env: &minijinja::Environment, + ctx: &C, + ) -> Result; +} + +impl Template for String { + type Rendered = String; + + fn render( + &self, + env: &minijinja::Environment, + ctx: &C, + ) -> Result { + env.render_str(self, ctx) + } +} + +impl> Template for Option { + type Rendered = Option; + + fn render( + &self, + env: &minijinja::Environment, + ctx: &C, + ) -> Result { + match self { + Some(template) => Ok(Some(template.render(env, ctx)?)), + None => Ok(None), + } + } +} + +impl> Template for Vec { + type Rendered = Vec; + + fn render( + &self, + env: &minijinja::Environment, + ctx: &C, + ) -> Result { + self.iter().map(|elem| elem.render(env, ctx)).collect() + } +} + +impl> Template for HashMap { + type Rendered = HashMap; + + fn render( + &self, + env: &minijinja::Environment, + ctx: &C, + ) -> Result { + let mut rendered = HashMap::::with_capacity(self.len()); + for (key, template) in self { + rendered.insert(key.clone(), template.render(env, ctx)?); + } + Ok(rendered) + } +} + +impl Template for app_raw::ExternalCommand { + type Rendered = app_raw::ExternalCommand; + + fn render( + &self, + env: &minijinja::Environment, + ctx: &C, + ) -> Result { + Ok(app_raw::ExternalCommand { + command: self.command.render(env, ctx)?, + dir: self.dir.render(env, ctx)?, + sources: self.sources.render(env, ctx)?, + targets: self.targets.render(env, ctx)?, + }) + } +} + +impl Template for serde_json::Value { + type Rendered = serde_json::Value; + + #[allow(clippy::only_used_in_recursion)] + fn render( + &self, + env: &minijinja::Environment, + ctx: &C, + ) -> Result { + Ok(match self { + value @ serde_json::Value::Null => value.clone(), + value @ serde_json::Value::Bool(_) => value.clone(), + value @ serde_json::Value::Number(_) => value.clone(), + value @ serde_json::Value::String(_) => value.clone(), + serde_json::Value::Array(elems) => { + let mut rendered_elems = Vec::::with_capacity(elems.len()); + for template in elems { + rendered_elems.push(template.render(env, ctx)?); + } + serde_json::Value::Array(rendered_elems) + } + serde_json::Value::Object(props) => { + let mut rendered_props = + serde_json::Map::::with_capacity(props.len()); + for (name, template) in props { + rendered_props.insert(name.clone(), template.render(env, ctx)?); + } + serde_json::Value::Object(rendered_props) + } + }) + } +} + +impl Template for serde_json::Map { + type Rendered = serde_json::Map; + + fn render( + &self, + env: &minijinja::Environment, + ctx: &C, + ) -> Result { + let mut rendered = serde_json::Map::::with_capacity(self.len()); + for (key, template) in self { + rendered.insert(key.clone(), template.render(env, ctx)?); + } + Ok(rendered) + } +} + +impl Template for app_raw::ComponentProperties { + type Rendered = app_raw::ComponentProperties; + + fn render( + &self, + env: &minijinja::Environment, + ctx: &C, + ) -> Result { + Ok(app_raw::ComponentProperties { + source_wit: self.source_wit.render(env, ctx)?, + generated_wit: self.generated_wit.render(env, ctx)?, + component_wasm: self.component_wasm.render(env, ctx)?, + linked_wasm: self.linked_wasm.render(env, ctx)?, + build: self.build.render(env, ctx)?, + custom_commands: self.custom_commands.render(env, ctx)?, + clean: self.clean.render(env, ctx)?, + extensions: self.extensions.render(env, ctx)?, + }) + } +} diff --git a/wasm-rpc-stubgen/src/model/unknown_properties.rs b/wasm-rpc-stubgen/src/model/unknown_properties.rs deleted file mode 100644 index 30519dea..00000000 --- a/wasm-rpc-stubgen/src/model/unknown_properties.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::model::validation::ValidationBuilder; -use std::collections::BTreeMap; - -pub type UnknownProperties = BTreeMap; - -pub trait HasUnknownProperties { - fn unknown_properties(&self) -> &UnknownProperties; - - fn add_unknown_property_warns(&self, context: F, validation_builder: &mut ValidationBuilder) - where - F: FnOnce() -> Vec<(&'static str, String)>, - { - let unknown_properties = self.unknown_properties(); - if unknown_properties.is_empty() { - return; - } - - let context = context(); - let context_count = context.len(); - - for context in context { - validation_builder.push_context(context.0, context.1); - } - - validation_builder.add_warns(self.unknown_properties().keys(), |unknown_property_name| { - Some(( - vec![], - format!("Unknown property: {}", unknown_property_name), - )) - }); - - for _ in 0..context_count { - validation_builder.pop_context(); - } - } -} diff --git a/wasm-rpc-stubgen/src/model/wasm_rpc.rs b/wasm-rpc-stubgen/src/model/wasm_rpc.rs deleted file mode 100644 index 2f6f4ad9..00000000 --- a/wasm-rpc-stubgen/src/model/wasm_rpc.rs +++ /dev/null @@ -1,969 +0,0 @@ -use crate::model::oam; -use crate::model::oam::TypedTraitProperties; -use crate::model::unknown_properties::{HasUnknownProperties, UnknownProperties}; -use crate::model::validation::{ValidatedResult, ValidationBuilder}; -use crate::naming; -use golem_wasm_rpc::WASM_RPC_VERSION; -use itertools::Itertools; -use serde::{Deserialize, Serialize}; -use std::collections::{BTreeMap, BTreeSet}; -use std::fs; -use std::path::{Path, PathBuf}; - -pub const DEFAULT_CONFIG_FILE_NAME: &str = "golem.yaml"; - -pub const OAM_TRAIT_TYPE_WASM_RPC: &str = "wasm-rpc"; - -pub const OAM_COMPONENT_TYPE_WASM: &str = "wasm"; -pub const OAM_COMPONENT_TYPE_WASM_BUILD: &str = "wasm-build"; -pub const OAM_COMPONENT_TYPE_WASM_RPC_STUB_BUILD: &str = "wasm-rpc-stub-build"; - -pub fn init_oam_app(_component_name: String) -> oam::Application { - // TODO: let's do it as part of https://github.com/golemcloud/wasm-rpc/issues/89 - todo!() -} - -// This a lenient non-validating peek for the include build property, -// as that is used early, during source collection -pub fn include_glob_patter_from_yaml_file(source: &Path) -> Option { - fs::read_to_string(source) - .ok() - .and_then(|source| oam::Application::from_yaml_str(source.as_str()).ok()) - .and_then(|mut oam_app| { - let mut includes = oam_app - .spec - .extract_components_by_type(&BTreeSet::from([OAM_COMPONENT_TYPE_WASM_BUILD])) - .remove(OAM_COMPONENT_TYPE_WASM_BUILD) - .unwrap_or_default() - .into_iter() - .filter_map(|component| { - component - .typed_properties::() - .ok() - .and_then(|properties| properties.include) - }); - - match includes.next() { - Some(include) => { - // Only return it if it's unique (if not it will cause validation errors later) - includes.next().is_none().then_some(include) - } - None => None, - } - }) -} - -#[derive(Clone, Debug)] -pub struct Application { - pub common_wasm_build: Option, - pub common_wasm_rpc_stub_build: Option, - pub wasm_rpc_stub_builds_by_name: BTreeMap, - pub wasm_components_by_name: BTreeMap, -} - -impl Application { - pub fn from_oam_apps(oam_apps: Vec) -> ValidatedResult { - let mut validation = ValidationBuilder::new(); - - let (all_components, all_wasm_builds, all_wasm_rpc_stub_builds) = { - let mut all_components = Vec::::new(); - let mut all_wasm_builds = Vec::::new(); - let mut all_wasm_rpc_stub_builds = Vec::::new(); - - for mut oam_app in oam_apps { - let (components, wasm_build, wasm_rpc_stub_build) = - Self::extract_and_convert_oam_components(&mut validation, &mut oam_app); - all_components.extend(components); - all_wasm_builds.extend(wasm_build); - all_wasm_rpc_stub_builds.extend(wasm_rpc_stub_build); - } - - (all_components, all_wasm_builds, all_wasm_rpc_stub_builds) - }; - - let wasm_components_by_name = Self::validate_components(&mut validation, all_components); - - let (common_wasm_rpc_stub_build, wasm_rpc_stub_builds_by_name) = - Self::validate_wasm_rpc_stub_builds( - &mut validation, - &wasm_components_by_name, - all_wasm_rpc_stub_builds, - ); - - let common_wasm_build = Self::validate_wasm_builds(&mut validation, all_wasm_builds); - - validation.build(Self { - common_wasm_build, - common_wasm_rpc_stub_build, - wasm_rpc_stub_builds_by_name, - wasm_components_by_name, - }) - } - - fn extract_and_convert_oam_components( - validation: &mut ValidationBuilder, - oam_app: &mut oam::ApplicationWithSource, - ) -> (Vec, Vec, Vec) { - validation.push_context("source", oam_app.source_as_string()); - - let mut components_by_type = - oam_app - .application - .spec - .extract_components_by_type(&BTreeSet::from([ - OAM_COMPONENT_TYPE_WASM, - OAM_COMPONENT_TYPE_WASM_BUILD, - OAM_COMPONENT_TYPE_WASM_RPC_STUB_BUILD, - ])); - - let wasm_components = Self::convert_components( - &oam_app.source, - validation, - &mut components_by_type, - OAM_COMPONENT_TYPE_WASM, - Self::convert_wasm_component, - ); - - let wasm_builds = Self::convert_components( - &oam_app.source, - validation, - &mut components_by_type, - OAM_COMPONENT_TYPE_WASM_BUILD, - Self::convert_wasm_build, - ); - - let wasm_rpc_stub_builds = Self::convert_components( - &oam_app.source, - validation, - &mut components_by_type, - OAM_COMPONENT_TYPE_WASM_RPC_STUB_BUILD, - Self::convert_wasm_rpc_stub_build, - ); - - validation.add_warns(&oam_app.application.spec.components, |component| { - Some(( - vec![ - ("component name", component.name.clone()), - ("component type", component.component_type.clone()), - ], - "Unknown component-type".to_string(), - )) - }); - - validation.pop_context(); - - (wasm_components, wasm_builds, wasm_rpc_stub_builds) - } - - fn convert_components( - source: &Path, - validation: &mut ValidationBuilder, - components_by_type: &mut BTreeMap<&'static str, Vec>, - component_type: &str, - convert: F, - ) -> Vec - where - F: Fn(&Path, &mut ValidationBuilder, oam::Component) -> Option, - { - components_by_type - .remove(component_type) - .unwrap_or_default() - .into_iter() - .filter_map(|component| { - validation.push_context("component name", component.name.clone()); - validation.push_context("component type", component.component_type.clone()); - let result = convert(source, validation, component); - validation.pop_context(); - validation.pop_context(); - result - }) - .collect() - } - - fn convert_wasm_component( - source: &Path, - validation: &mut ValidationBuilder, - mut component: oam::Component, - ) -> Option { - let properties = component.typed_properties::(); - - if let Some(err) = properties.as_ref().err() { - validation.add_error(format!("Failed to get component properties: {}", err)) - } - - let wasm_rpc_traits = component - .extract_traits_by_type(&BTreeSet::from([OAM_TRAIT_TYPE_WASM_RPC])) - .remove(OAM_TRAIT_TYPE_WASM_RPC) - .unwrap_or_default(); - - let mut wasm_rpc_dependencies = Vec::::new(); - for wasm_rpc in wasm_rpc_traits { - validation.push_context("trait type", wasm_rpc.trait_type.clone()); - - match WasmRpcTraitProperties::from_generic_trait(wasm_rpc) { - Ok(wasm_rpc) => { - wasm_rpc.add_unknown_property_warns( - || vec![("dep component name", wasm_rpc.component_name.clone())], - validation, - ); - wasm_rpc_dependencies.push(wasm_rpc.component_name) - } - Err(err) => validation - .add_error(format!("Failed to get wasm-rpc trait properties: {}", err)), - } - - validation.pop_context(); - } - - let non_unique_wasm_rpc_dependencies = wasm_rpc_dependencies - .iter() - .counts() - .into_iter() - .filter(|(_, count)| *count > 1); - validation.add_warns( - non_unique_wasm_rpc_dependencies, - |(dep_component_name, count)| { - Some(( - vec![], - format!( - "WASM RPC dependency specified multiple times for component: {}, count: {}", - dep_component_name, count - ), - )) - }, - ); - - validation.add_warns(component.traits, |component_trait| { - Some(( - vec![], - format!( - "Unknown trait for wasm component, trait type: {}", - component_trait.trait_type - ), - )) - }); - - let wasm_rpc_dependencies = wasm_rpc_dependencies - .into_iter() - .unique() - .sorted() - .collect::>(); - - match (properties, validation.has_any_errors()) { - (Ok(properties), false) => { - properties.add_unknown_property_warns(Vec::new, validation); - - for build_step in &properties.build { - let has_inputs = !build_step.inputs.is_empty(); - let has_outputs = !build_step.outputs.is_empty(); - - if (has_inputs && !has_outputs) || (!has_inputs && has_outputs) { - validation.push_context("command", build_step.command.clone()); - validation.add_warn( - "Using inputs and outputs only has effect when both defined" - .to_string(), - ); - validation.pop_context(); - } - } - - Some(WasmComponent { - name: component.name, - source: source.to_path_buf(), - build_steps: properties.build, - wit: properties.wit.into(), - input_wasm: properties.input_wasm.into(), - output_wasm: properties.output_wasm.into(), - wasm_rpc_dependencies, - }) - } - _ => None, - } - } - - fn convert_wasm_build( - source: &Path, - validation: &mut ValidationBuilder, - component: oam::Component, - ) -> Option { - let result = match component.typed_properties::() { - Ok(properties) => { - properties.add_unknown_property_warns(Vec::new, validation); - - let wasm_rpc_stub_build = WasmBuild { - source: source.to_path_buf(), - name: component.name, - build_dir: properties.build_dir.map(|s| s.into()), - }; - - Some(wasm_rpc_stub_build) - } - Err(err) => { - validation.add_error(format!("Failed to get wasm build properties: {}", err)); - None - } - }; - - validation.add_warns(component.traits, |component_trait| { - Some(( - vec![], - format!( - "Unknown trait for wasm build, trait type: {}", - component_trait.trait_type - ), - )) - }); - - result - } - - fn convert_wasm_rpc_stub_build( - source: &Path, - validation: &mut ValidationBuilder, - component: oam::Component, - ) -> Option { - let result = match component.typed_properties::() { - Ok(properties) => { - properties.add_unknown_property_warns(Vec::new, validation); - - let wasm_rpc_stub_build = WasmRpcStubBuild { - source: source.to_path_buf(), - name: component.name, - component_name: properties.component_name, - build_dir: properties.build_dir.map(|s| s.into()), - wasm: properties.wasm.map(|s| s.into()), - wit: properties.wit.map(|s| s.into()), - world: properties.world, - always_inline_types: properties.always_inline_types, - crate_version: properties.crate_version, - wasm_rpc_path: properties.wasm_rpc_path, - wasm_rpc_version: properties.wasm_rpc_version, - }; - - if wasm_rpc_stub_build.build_dir.is_some() && wasm_rpc_stub_build.wasm.is_some() { - validation.add_warn( - "Both buildDir and wasm fields are defined, wasm takes precedence" - .to_string(), - ); - } - - if wasm_rpc_stub_build.build_dir.is_some() && wasm_rpc_stub_build.wit.is_some() { - validation.add_warn( - "Both buildDir and wit fields are defined, wit takes precedence" - .to_string(), - ); - } - - if wasm_rpc_stub_build.component_name.is_some() - && wasm_rpc_stub_build.wasm.is_some() - { - validation.add_warn( - "In common (without component name) wasm rpc stub build the wasm field has no effect".to_string(), - ); - } - - if wasm_rpc_stub_build.component_name.is_some() && wasm_rpc_stub_build.wit.is_some() - { - validation.add_warn( - "In common (without component name) wasm rpc stub build the wit field has no effect".to_string(), - ); - } - - Some(wasm_rpc_stub_build) - } - Err(err) => { - validation.add_error(format!( - "Failed to get wasm rpc stub build properties: {}", - err - )); - None - } - }; - - validation.add_warns(component.traits, |component_trait| { - Some(( - vec![], - format!( - "Unknown trait for wasm rpc stub build, trait type: {}", - component_trait.trait_type - ), - )) - }); - - result - } - - pub fn validate_components( - validation: &mut ValidationBuilder, - components: Vec, - ) -> BTreeMap { - let (wasm_components_by_name, sources) = { - let mut wasm_components_by_name = BTreeMap::::new(); - let mut sources = BTreeMap::>::new(); - for component in components { - sources - .entry(component.name.clone()) - .and_modify(|sources| sources.push(component.source_as_string())) - .or_insert_with(|| vec![component.source_as_string()]); - wasm_components_by_name.insert(component.name.clone(), component); - } - (wasm_components_by_name, sources) - }; - - let non_unique_components = sources.into_iter().filter(|(_, sources)| sources.len() > 1); - validation.add_errors(non_unique_components, |(component_name, sources)| { - Some(( - vec![("component name", component_name)], - format!( - "Component is specified multiple times in sources: {}", - sources.join(", ") - ), - )) - }); - - for (component_name, component) in &wasm_components_by_name { - validation.push_context("source", component.source_as_string()); - - validation.add_errors(&component.wasm_rpc_dependencies, |dep_component_name| { - (!wasm_components_by_name.contains_key(dep_component_name)).then(|| { - ( - vec![], - format!( - "Component {} references unknown component {} as dependency", - component_name, dep_component_name, - ), - ) - }) - }); - - validation.pop_context(); - } - - wasm_components_by_name - } - - fn validate_wasm_builds( - validation: &mut ValidationBuilder, - wasm_builds: Vec, - ) -> Option { - if wasm_builds.len() > 1 { - validation.add_error(format!( - "Component Build is specified multiple times in sources: {}", - wasm_builds - .iter() - .map(|c| format!("{} in {}", c.name, c.source.to_string_lossy())) - .join(", ") - )); - } - - wasm_builds.into_iter().next() - } - - fn validate_wasm_rpc_stub_builds( - validation: &mut ValidationBuilder, - wasm_components_by_name: &BTreeMap, - wasm_rpc_stub_builds: Vec, - ) -> (Option, BTreeMap) { - let ( - common_wasm_rpc_stub_builds, - wasm_rpc_stub_builds_by_component_name, - common_sources, - sources, - ) = { - let mut common_wasm_rpc_stub_builds = Vec::::new(); - let mut wasm_rpc_stub_builds_by_component_name = - BTreeMap::::new(); - - let mut common_sources = Vec::::new(); - let mut by_name_sources = BTreeMap::>::new(); - - for wasm_rpc_stub_build in wasm_rpc_stub_builds { - match &wasm_rpc_stub_build.component_name { - Some(component_name) => { - by_name_sources - .entry(component_name.clone()) - .and_modify(|sources| { - sources.push(wasm_rpc_stub_build.source_as_string()) - }) - .or_insert_with(|| vec![wasm_rpc_stub_build.source_as_string()]); - wasm_rpc_stub_builds_by_component_name - .insert(component_name.clone(), wasm_rpc_stub_build); - } - None => { - common_sources.push(wasm_rpc_stub_build.source_as_string()); - common_wasm_rpc_stub_builds.push(wasm_rpc_stub_build) - } - } - } - - ( - common_wasm_rpc_stub_builds, - wasm_rpc_stub_builds_by_component_name, - common_sources, - by_name_sources, - ) - }; - - let non_unique_wasm_rpc_stub_builds = - sources.into_iter().filter(|(_, sources)| sources.len() > 1); - - validation.add_errors( - non_unique_wasm_rpc_stub_builds, - |(component_name, sources)| { - Some(( - vec![("component name", component_name)], - format!( - "Wasm rpc stub build is specified multiple times in sources: {}", - sources.join(", ") - ), - )) - }, - ); - - if common_sources.len() > 1 { - validation.add_error( - format!( - "Common (without component name) wasm rpc build is specified multiple times in sources: {}", - common_sources.join(", "), - ) - ) - } - - validation.add_errors( - &wasm_rpc_stub_builds_by_component_name, - |(component_name, wasm_rpc_stub_build)| { - (!wasm_components_by_name.contains_key(component_name)).then(|| { - ( - vec![("source", wasm_rpc_stub_build.source_as_string())], - format!( - "Wasm rpc stub build {} references unknown component {}", - wasm_rpc_stub_build.name, component_name - ), - ) - }) - }, - ); - - ( - common_wasm_rpc_stub_builds.into_iter().next(), - wasm_rpc_stub_builds_by_component_name, - ) - } - - pub fn all_wasm_rpc_dependencies(&self) -> BTreeSet { - self.wasm_components_by_name - .iter() - .flat_map(|(_, component)| { - component - .wasm_rpc_dependencies - .iter() - .map(|component_name| component_name.to_string()) - }) - .collect() - } - - pub fn build_dir(&self) -> PathBuf { - self.common_wasm_build - .as_ref() - .and_then(|build| build.build_dir.clone()) - .unwrap_or_else(|| PathBuf::from("build")) - } - - pub fn component(&self, component_name: &str) -> &WasmComponent { - self.wasm_components_by_name - .get(component_name) - .unwrap_or_else(|| panic!("Component not found: {}", component_name)) - } - - pub fn component_wit(&self, component_name: &str) -> PathBuf { - let component = self.component(component_name); - component.source_dir().join(component.wit.clone()) - } - - pub fn component_input_wasm(&self, component_name: &str) -> PathBuf { - let component = self.component(component_name); - component.source_dir().join(component.input_wasm.clone()) - } - - pub fn component_output_wasm(&self, component_name: &str) -> PathBuf { - let component = self.component(component_name); - component.source_dir().join(component.output_wasm.clone()) - } - - pub fn stub_world(&self, component_name: &str) -> Option { - self.stub_gen_property(component_name, |build| build.world.clone()) - .flatten() - } - - pub fn stub_crate_version(&self, component_name: &str) -> String { - self.stub_gen_property(component_name, |build| build.crate_version.clone()) - .flatten() - .unwrap_or_else(|| WASM_RPC_VERSION.to_string()) - } - - pub fn stub_always_inline_types(&self, component_name: &str) -> bool { - self.stub_gen_property(component_name, |build| build.always_inline_types) - .flatten() - .unwrap_or(false) - } - - pub fn stub_wasm_rpc_path(&self, component_name: &str) -> Option { - self.stub_gen_property(component_name, |build| build.wasm_rpc_path.clone()) - .flatten() - } - - pub fn stub_wasm_rpc_version(&self, component_name: &str) -> Option { - self.stub_gen_property(component_name, |build| build.wasm_rpc_version.clone()) - .flatten() - } - - pub fn stub_build_dir(&self, component_name: &str) -> PathBuf { - self.stub_gen_property(component_name, |build| { - build - .build_dir - .as_ref() - .map(|build_dir| build.source_dir().join(build_dir)) - }) - .flatten() - .unwrap_or_else(|| self.build_dir()) - .join("stub") - } - - pub fn stub_wasm(&self, component_name: &str) -> PathBuf { - self.wasm_rpc_stub_builds_by_name - .get(component_name) - .and_then(|build| { - build - .wasm - .as_ref() - .map(|wasm| build.source_dir().join(wasm)) - }) - .unwrap_or_else(|| { - self.stub_build_dir(component_name) - .join(component_name) - .join("stub.wasm") - }) - } - - pub fn stub_wit(&self, component_name: &str) -> PathBuf { - self.wasm_rpc_stub_builds_by_name - .get(component_name) - .and_then(|build| build.wit.as_ref().map(|wit| build.source_dir().join(wit))) - .unwrap_or_else(|| { - self.stub_build_dir(component_name) - .join(component_name) - .join(naming::wit::WIT_DIR) - }) - } - - fn stub_gen_property(&self, component_name: &str, get_property: F) -> Option - where - F: Fn(&WasmRpcStubBuild) -> T, - { - self.wasm_rpc_stub_builds_by_name - .get(component_name) - .map(&get_property) - .or_else(|| self.common_wasm_rpc_stub_build.as_ref().map(get_property)) - } -} - -#[derive(Clone, Debug)] -pub struct WasmComponent { - pub name: String, - pub source: PathBuf, - pub build_steps: Vec, - pub wit: PathBuf, - pub input_wasm: PathBuf, - pub output_wasm: PathBuf, - pub wasm_rpc_dependencies: Vec, -} - -impl WasmComponent { - pub fn source_as_string(&self) -> String { - self.source.to_string_lossy().to_string() - } - - pub fn source_dir(&self) -> &Path { - self.source - .parent() - .expect("Failed to get parent for source") - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct BuildStep { - pub command: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub dir: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub inputs: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub outputs: Vec, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct WasmComponentProperties { - pub wit: String, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub build: Vec, - #[serde(rename = "inputWasm")] - pub input_wasm: String, - #[serde(rename = "outputWasm")] - pub output_wasm: String, - #[serde(flatten)] - pub unknown_properties: UnknownProperties, -} - -impl HasUnknownProperties for WasmComponentProperties { - fn unknown_properties(&self) -> &UnknownProperties { - &self.unknown_properties - } -} - -impl oam::TypedComponentProperties for WasmComponentProperties { - fn component_type() -> &'static str { - OAM_COMPONENT_TYPE_WASM - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct WasmRpcTraitProperties { - #[serde(rename = "componentName")] - pub component_name: String, - #[serde(flatten)] - pub unknown_properties: UnknownProperties, -} - -impl HasUnknownProperties for WasmRpcTraitProperties { - fn unknown_properties(&self) -> &UnknownProperties { - &self.unknown_properties - } -} - -impl oam::TypedTraitProperties for WasmRpcTraitProperties { - fn trait_type() -> &'static str { - OAM_TRAIT_TYPE_WASM_RPC - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ComponentBuildProperties { - include: Option, - build_dir: Option, - #[serde(flatten)] - unknown_properties: UnknownProperties, -} - -impl oam::TypedComponentProperties for ComponentBuildProperties { - fn component_type() -> &'static str { - OAM_COMPONENT_TYPE_WASM_BUILD - } -} - -impl HasUnknownProperties for ComponentBuildProperties { - fn unknown_properties(&self) -> &UnknownProperties { - &self.unknown_properties - } -} - -#[derive(Clone, Debug)] -pub struct WasmBuild { - source: PathBuf, - name: String, - build_dir: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ComponentStubBuildProperties { - component_name: Option, - build_dir: Option, - wasm: Option, - wit: Option, - world: Option, - always_inline_types: Option, - crate_version: Option, - wasm_rpc_path: Option, - wasm_rpc_version: Option, - #[serde(flatten)] - unknown_properties: UnknownProperties, -} - -impl oam::TypedComponentProperties for ComponentStubBuildProperties { - fn component_type() -> &'static str { - OAM_COMPONENT_TYPE_WASM_RPC_STUB_BUILD - } -} - -impl HasUnknownProperties for ComponentStubBuildProperties { - fn unknown_properties(&self) -> &UnknownProperties { - &self.unknown_properties - } -} - -#[derive(Clone, Debug)] -pub struct WasmRpcStubBuild { - source: PathBuf, - name: String, - component_name: Option, - build_dir: Option, - wasm: Option, - wit: Option, - world: Option, - always_inline_types: Option, - crate_version: Option, - wasm_rpc_path: Option, - wasm_rpc_version: Option, -} - -impl WasmRpcStubBuild { - pub fn source_as_string(&self) -> String { - self.source.to_string_lossy().to_string() - } - - pub fn source_dir(&self) -> &Path { - self.source - .parent() - .expect("Failed to get parent for source") - } -} - -#[cfg(test)] -mod tests { - use test_r::test; - - use super::*; - use crate::model::oam; - use assert2::assert; - - #[test] - fn load_app_with_warns() { - let oam_app: oam::ApplicationWithSource = oam_app_one(); - let (app, warns, errors) = Application::from_oam_apps(vec![oam_app]).into_product(); - - assert!(app.is_some()); - let app = app.unwrap(); - - println!("Warns:\n{}", warns.join("\n")); - println!("Errors:\n{}", errors.join("\n")); - - assert!(app.wasm_components_by_name.len() == 3); - assert!(warns.len() == 3); - assert!(errors.len() == 0); - - let (component_name, component) = app.wasm_components_by_name.iter().next().unwrap(); - - assert!(component_name == "component-one"); - assert!(component.name == "component-one"); - assert!(component.wit.to_string_lossy() == "wit"); - assert!(component.input_wasm.to_string_lossy() == "out/in.wasm"); - assert!(component.output_wasm.to_string_lossy() == "out/out.wasm"); - assert!(component.wasm_rpc_dependencies.len() == 2); - - assert!(component.wasm_rpc_dependencies[0] == "component-three"); - assert!(component.wasm_rpc_dependencies[1] == "component-two"); - } - - #[test] - fn load_app_with_warns_and_errors() { - let oam_app: oam::ApplicationWithSource = oam_app_two(); - let (_app, warns, errors) = Application::from_oam_apps(vec![oam_app]).into_product(); - - println!("Warns:\n{}", warns.join("\n")); - println!("Errors:\n{}", errors.join("\n")); - - assert!(errors.len() == 2); - - assert!(errors[0].contains("component-one")); - assert!(errors[0].contains("component-three")); - assert!(errors[0].contains("test-oam-app-two.yaml")); - - assert!(errors[1].contains("component-one")); - assert!(errors[1].contains("component-two")); - assert!(errors[1].contains("test-oam-app-two.yaml")); - } - - fn oam_app_one() -> oam::ApplicationWithSource { - oam::ApplicationWithSource::from_yaml_string( - "test-oam-app-one.yaml".into(), - r#" -apiVersion: core.oam.dev/v1beta1 -metadata: - name: "App name" -kind: Application -spec: - components: - - name: component-one - type: wasm - properties: - wit: wit - inputWasm: out/in.wasm - outputWasm: out/out.wasm - testUnknownProp: test - traits: - - type: wasm-rpc - properties: - componentName: component-two - - type: wasm-rpc - properties: - componentName: component-three - testUnknownProp: test - - type: unknown-trait - properties: - testUnknownProp: test - - name: component-two - type: wasm - properties: - wit: wit - inputWasm: out/in.wasm - outputWasm: out/out.wasm - - name: component-three - type: wasm - properties: - wit: wit - inputWasm: out/in.wasm - outputWasm: out/out.wasm -"# - .to_string(), - ) - .unwrap() - } - - fn oam_app_two() -> oam::ApplicationWithSource { - oam::ApplicationWithSource::from_yaml_string( - "test-oam-app-two.yaml".into(), - r#" -apiVersion: core.oam.dev/v1beta1 -metadata: - name: "App name" -kind: Application -spec: - components: - - name: component-one - type: wasm - properties: - wit: wit - inputWasm: out/in.wasm - outputWasm: out/out.wasm - testUnknownProp: test - traits: - - type: wasm-rpc - properties: - componentName: component-two - - type: wasm-rpc - properties: - componentName: component-three - testUnknownProp: test - - type: unknown-trait - properties: - testUnknownProp: test - - name: component-one - type: unknown-component-type - properties: -"# - .to_string(), - ) - .unwrap() - } -} diff --git a/wasm-rpc-stubgen/src/naming.rs b/wasm-rpc-stubgen/src/naming.rs index 5126e632..8860dc90 100644 --- a/wasm-rpc-stubgen/src/naming.rs +++ b/wasm-rpc-stubgen/src/naming.rs @@ -1,22 +1,59 @@ pub mod wit { + use crate::log::LogColorize; + use anyhow::{anyhow, bail}; use std::path::{Path, PathBuf}; - use wit_parser::PackageName; pub static DEPS_DIR: &str = "deps"; pub static WIT_DIR: &str = "wit"; pub static STUB_WIT_FILE_NAME: &str = "_stub.wit"; + pub static INTERFACE_WIT_FILE_NAME: &str = "_interface.wit"; - pub fn stub_package_name(package_name: &PackageName) -> PackageName { - PackageName { + pub fn stub_package_name(package_name: &wit_parser::PackageName) -> wit_parser::PackageName { + wit_parser::PackageName { namespace: package_name.namespace.clone(), name: format!("{}-stub", package_name.name), version: package_name.version.clone(), } } - pub fn stub_target_package_name(stub_package_name: &PackageName) -> PackageName { - PackageName { + pub fn interface_parser_package_name( + package_name: &wit_parser::PackageName, + ) -> wit_parser::PackageName { + wit_parser::PackageName { + namespace: package_name.namespace.clone(), + name: format!("{}-interface", package_name.name), + version: package_name.version.clone(), + } + } + + pub fn interface_encoder_package_name( + package_name: &wit_encoder::PackageName, + ) -> wit_encoder::PackageName { + wit_encoder::PackageName::new( + package_name.namespace(), + format!("{}-interface", package_name.name()), + package_name.version().cloned(), + ) + } + + pub fn interface_package_world_inline_interface_name( + world_name: &wit_encoder::Ident, + interface_name: &wit_encoder::Ident, + ) -> String { + format!("{}-{}", world_name.raw_name(), interface_name.raw_name()) + } + + pub fn interface_package_world_inline_functions_interface_name( + world_name: &wit_encoder::Ident, + ) -> String { + format!("{}-inline-functions", world_name.raw_name()) + } + + pub fn stub_target_package_name( + stub_package_name: &wit_parser::PackageName, + ) -> wit_parser::PackageName { + wit_parser::PackageName { namespace: stub_package_name.namespace.clone(), name: stub_package_name .name @@ -27,15 +64,69 @@ pub mod wit { } } - pub fn package_dep_dir_name(package_name: &PackageName) -> String { + pub fn stub_import_name(stub_package: &wit_parser::Package) -> anyhow::Result { + let package_name = &stub_package.name; + + if stub_package.interfaces.len() != 1 { + bail!( + "Expected exactly one interface in stub package, package name: {}", + package_name.to_string().log_color_highlight() + ); + } + + let interface_name = stub_package.interfaces.first().unwrap().0; + + Ok(format!( + "{}:{}/{}{}", + package_name.namespace, + package_name.name, + interface_name, + package_name + .version + .as_ref() + .map(|version| format!("@{}", version)) + .unwrap_or_default() + )) + } + + pub fn stub_import_interface_prefix_from_stub_package_name( + stub_package: &wit_parser::PackageName, + ) -> anyhow::Result { + Ok(format!( + "{}:{}-interface/", + stub_package.namespace, + stub_package + .name + .clone() + .strip_suffix("-stub") + .ok_or_else(|| anyhow!( + "Expected \"-stub\" suffix in stub package name: {}", + stub_package.to_string() + ))? + )) + } + + pub fn package_dep_dir_name_from_parser(package_name: &wit_parser::PackageName) -> String { format!("{}_{}", package_name.namespace, package_name.name) } + pub fn package_dep_dir_name_from_encoder(package_name: &wit_encoder::PackageName) -> String { + format!("{}_{}", package_name.namespace(), package_name.name()) + } + + pub fn package_merged_wit_name(package_name: &wit_parser::PackageName) -> String { + format!("{}_{}.wit", package_name.namespace, package_name.name) + } + pub fn package_wit_dep_dir_from_package_dir_name(package_dir_name: &str) -> PathBuf { Path::new(WIT_DIR).join(DEPS_DIR).join(package_dir_name) } - pub fn package_wit_dep_dir_from_package_name(package_name: &PackageName) -> PathBuf { - package_wit_dep_dir_from_package_dir_name(&package_dep_dir_name(package_name)) + pub fn package_wit_dep_dir_from_parser(package_name: &wit_parser::PackageName) -> PathBuf { + package_wit_dep_dir_from_package_dir_name(&package_dep_dir_name_from_parser(package_name)) + } + + pub fn package_wit_dep_dir_from_encode(package_name: &wit_encoder::PackageName) -> PathBuf { + package_wit_dep_dir_from_package_dir_name(&package_dep_dir_name_from_encoder(package_name)) } } diff --git a/wasm-rpc-stubgen/src/rust.rs b/wasm-rpc-stubgen/src/rust.rs index 692aa6da..38885fc7 100644 --- a/wasm-rpc-stubgen/src/rust.rs +++ b/wasm-rpc-stubgen/src/rust.rs @@ -12,13 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::commands::log::log_action; -use crate::stub::{FunctionResultStub, FunctionStub, InterfaceStub, StubDefinition, StubTypeOwner}; +use crate::fs; +use crate::fs::PathExtra; +use crate::log::{log_action, LogColorize}; +use crate::stub::{FunctionResultStub, FunctionStub, InterfaceStub, StubDefinition}; use anyhow::anyhow; use heck::{ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase}; use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; -use std::fs; use wit_bindgen_rust::to_rust_ident; use wit_parser::{ Enum, Flags, Handle, Record, Result_, Tuple, Type, TypeDef, TypeDefKind, TypeOwner, Variant, @@ -44,7 +45,7 @@ pub fn generate_stub_source(def: &StubDefinition) -> anyhow::Result<()> { let mut exports = Vec::new(); let mut resource_type_aliases = Vec::new(); - for interface in def.source_interfaces() { + for interface in def.stub_imported_interfaces() { let interface_ident = to_rust_ident(&interface.name).to_upper_camel_case(); let interface_name = Ident::new(&interface_ident, Span::call_site()); @@ -116,7 +117,7 @@ pub fn generate_stub_source(def: &StubDefinition) -> anyhow::Result<()> { } let mut interface_impls = Vec::new(); - for interface in def.source_interfaces() { + for interface in def.stub_imported_interfaces() { let interface_ident = to_rust_ident(&interface.name).to_upper_camel_case(); let interface_name = Ident::new(&interface_ident, Span::call_site()); let guest_interface_name = @@ -236,8 +237,8 @@ pub fn generate_stub_source(def: &StubDefinition) -> anyhow::Result<()> { let remote_function_name = get_remote_function_name( def, "drop", - interface.interface_name().as_ref(), - interface.resource_name().as_ref(), + interface.interface_name(), + interface.resource_name(), ); if interface.is_resource() { interface_impls.push(quote! { @@ -284,14 +285,13 @@ pub fn generate_stub_source(def: &StubDefinition) -> anyhow::Result<()> { let syntax_tree = syn::parse2(lib)?; let src = prettyplease::unparse(&syntax_tree); + let target_rust_path = PathExtra::new(def.target_rust_path()); + log_action( "Generating", - format!( - "stub source to {}", - def.target_rust_path().to_string_lossy() - ), + format!("stub source to {}", target_rust_path.log_color_highlight()), ); - fs::create_dir_all(def.target_rust_path().parent().unwrap())?; + fs::create_dir_all(target_rust_path.parent()?)?; fs::write(def.target_rust_path(), src)?; Ok(()) } @@ -331,8 +331,8 @@ fn generate_result_wrapper_get_source( let remote_function_name = get_remote_function_name( def, &function.name, - interface.interface_name().as_ref(), - interface.resource_name().as_ref(), + interface.interface_name(), + interface.resource_name(), ); Ok(quote! { @@ -392,8 +392,8 @@ fn generate_function_stub_source( let remote_function_name = get_remote_function_name( def, &function.name, - owner.interface_name().as_ref(), - owner.resource_name().as_ref(), + owner.interface_name(), + owner.resource_name(), ); let rpc = match mode { @@ -518,14 +518,14 @@ fn get_output_values_source( ) -> anyhow::Result> { let mut output_values = Vec::new(); match &function.results { - FunctionResultStub::Single(typ) => { + FunctionResultStub::Anon(typ) => { output_values.push(extract_from_wit_value( typ, def, quote! { result.tuple_element(0).expect("tuple not found") }, )?); } - FunctionResultStub::Multi(params) => { + FunctionResultStub::Named(params) => { for (n, param) in params.iter().enumerate() { output_values.push(extract_from_wit_value( ¶m.typ, @@ -560,13 +560,13 @@ fn get_result_type_source( function: &FunctionStub, ) -> anyhow::Result { let result_type = match &function.results { - FunctionResultStub::Single(typ) => { + FunctionResultStub::Anon(typ) => { let typ = type_to_rust_ident(typ, def)?; quote! { #typ } } - FunctionResultStub::Multi(params) => { + FunctionResultStub::Named(params) => { let mut results = Vec::new(); for param in params { let param_name = Ident::new(&to_rust_ident(¶m.name), Span::call_site()); @@ -593,8 +593,8 @@ fn get_result_type_source( fn get_remote_function_name( def: &StubDefinition, function_name: &str, - interface_name: Option<&String>, - resource_name: Option<&String>, + interface_name: Option<&str>, + resource_name: Option<&str>, ) -> String { match (interface_name, resource_name) { (Some(remote_interface), None) => { @@ -716,9 +716,8 @@ fn type_to_rust_ident(typ: &Type, def: &StubDefinition) -> anyhow::Result { + match &type_def.owner { + TypeOwner::World(world_id) => { let world = def.get_world(*world_id)?; let package_id = world.package.ok_or(anyhow!("world has no package"))?; @@ -732,7 +731,7 @@ fn type_to_rust_ident(typ: &Type, def: &StubDefinition) -> anyhow::Result { + TypeOwner::Interface(interface_id) => { let interface = def.get_interface(*interface_id)?; let package_id = interface .package @@ -754,27 +753,7 @@ fn type_to_rust_ident(typ: &Type, def: &StubDefinition) -> anyhow::Result {} - StubTypeOwner::StubInterface => { - let root_ns = Ident::new( - &def.source_package_name.namespace.to_snake_case(), - Span::call_site(), - ); - let root_name = Ident::new( - &format!("{}_stub", def.source_package_name.name.to_snake_case()), - Span::call_site(), - ); - let stub_interface_name = format!("stub-{}", def.source_world_name()); - let stub_interface_name = Ident::new( - &to_rust_ident(&stub_interface_name).to_snake_case(), - Span::call_site(), - ); - - path.push(quote! { exports }); - path.push(quote! { #root_ns }); - path.push(quote! { #root_name }); - path.push(quote! { #stub_interface_name }); - } + TypeOwner::None => {} } Ok(quote! { #(#path)::*::#typ }) } @@ -1308,10 +1287,10 @@ fn extract_from_wit_value( extract_from_result_value(result, def, base_expr) } TypeDefKind::List(elem) => extract_from_list_value(elem, def, base_expr), - TypeDefKind::Future(_) => Ok(quote!(todo!("future"))), - TypeDefKind::Stream(_) => Ok(quote!(todo!("stream"))), + TypeDefKind::Future(_) => Ok(quote!(panic!("Future is not supported yet"))), + TypeDefKind::Stream(_) => Ok(quote!(panic!("Stream is not supported yet"))), TypeDefKind::Type(typ) => extract_from_wit_value(typ, def, base_expr), - TypeDefKind::Unknown => Ok(quote!(todo!("unknown"))), + TypeDefKind::Unknown => Ok(quote!(panic!("Unexpected unknown type!"))), } } } diff --git a/wasm-rpc-stubgen/src/stub.rs b/wasm-rpc-stubgen/src/stub.rs index 2493fce7..87880073 100644 --- a/wasm-rpc-stubgen/src/stub.rs +++ b/wasm-rpc-stubgen/src/stub.rs @@ -12,46 +12,59 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::wit_resolve::ResolvedWitDir; +use crate::wit_encode::EncodedWitDir; +use crate::wit_generate::extract_main_interface_as_wit_dep; +use crate::wit_resolve::{PackageSource, ResolvedWitDir}; use crate::{naming, WasmRpcOverride}; -use anyhow::anyhow; +use anyhow::{anyhow, Context}; use indexmap::IndexMap; +use itertools::Itertools; use std::cell::OnceCell; -use std::path::{Path, PathBuf}; +use std::collections::{HashMap, HashSet}; +use std::path::PathBuf; use wit_parser::{ Function, FunctionKind, Interface, InterfaceId, Package, PackageId, PackageName, Resolve, Results, Type, TypeDef, TypeDefKind, TypeId, TypeOwner, World, WorldId, WorldItem, WorldKey, }; -/// All the gathered information for generating the stub crate. +#[derive(Clone, Debug)] +pub struct StubConfig { + pub source_wit_root: PathBuf, + pub target_root: PathBuf, + pub selected_world: Option, + pub stub_crate_version: String, + pub wasm_rpc_override: WasmRpcOverride, + pub extract_source_interface_package: bool, + pub seal_cargo_workspace: bool, +} + pub struct StubDefinition { + pub config: StubConfig, + resolve: Resolve, source_world_id: WorldId, - sources: IndexMap)>, - source_interfaces: OnceCell>, + package_sources: IndexMap, + stub_imported_interfaces: OnceCell>, + stub_used_type_defs: OnceCell>, + stub_dep_package_ids: OnceCell>, + + pub source_package_id: PackageId, pub source_package_name: PackageName, - pub source_wit_root: PathBuf, - pub target_root: PathBuf, - pub stub_crate_version: String, - pub wasm_rpc_override: WasmRpcOverride, - pub always_inline_types: bool, } impl StubDefinition { - pub fn new( - source_wit_root: &Path, - target_root: &Path, - selected_world: &Option, - stub_crate_version: &str, - wasm_rpc_override: &WasmRpcOverride, - always_inline_types: bool, - ) -> anyhow::Result { - let resolved_source = ResolvedWitDir::new(source_wit_root)?; + pub fn new(config: StubConfig) -> anyhow::Result { + if config.extract_source_interface_package { + extract_main_interface_as_wit_dep(&config.source_wit_root) + .context("Failed to extract the source interface package")? + } + + let resolved_source = ResolvedWitDir::new(&config.source_wit_root)?; let source_world_id = resolved_source .resolve - .select_world(resolved_source.package_id, selected_world.as_deref())?; + .select_world(resolved_source.package_id, config.selected_world.as_deref())?; let source_package_name = resolved_source .resolve @@ -67,16 +80,15 @@ impl StubDefinition { .clone(); Ok(Self { + config, resolve: resolved_source.resolve, source_world_id, - sources: resolved_source.sources, - source_interfaces: OnceCell::new(), + package_sources: resolved_source.package_sources, + stub_imported_interfaces: OnceCell::new(), + stub_used_type_defs: OnceCell::new(), + stub_dep_package_ids: OnceCell::new(), + source_package_id: resolved_source.package_id, source_package_name, - source_wit_root: source_wit_root.to_path_buf(), - target_root: target_root.to_path_buf(), - stub_crate_version: stub_crate_version.to_string(), - wasm_rpc_override: wasm_rpc_override.clone(), - always_inline_types, }) } @@ -92,17 +104,18 @@ impl StubDefinition { pub fn packages_with_wit_sources( &self, - ) -> impl Iterator))> { + ) -> impl Iterator { self.resolve .topological_packages() .into_iter() .map(|package_id| { ( + package_id, self.resolve .packages .get(package_id) .unwrap_or_else(|| panic!("package not found")), - self.sources + self.package_sources .get(&package_id) .unwrap_or_else(|| panic!("sources for package not found")), ) @@ -120,12 +133,16 @@ impl StubDefinition { .unwrap_or_else(|| panic!("selected world not found")) } - pub fn source_world_name(&self) -> String { - self.source_world().name.clone() + pub fn source_world_name(&self) -> &str { + &self.source_world().name + } + + pub fn encode_source(&self) -> anyhow::Result { + EncodedWitDir::new(&self.resolve) } pub fn target_cargo_path(&self) -> PathBuf { - self.target_root.join("Cargo.toml") + self.config.target_root.join("Cargo.toml") } pub fn target_crate_name(&self) -> String { @@ -133,15 +150,21 @@ impl StubDefinition { } pub fn target_rust_path(&self) -> PathBuf { - self.target_root.join("src/lib.rs") + self.config.target_root.join("src/lib.rs") + } + + pub fn target_interface_name(&self) -> String { + // TODO: naming + format!("stub-{}", self.source_world_name()) } pub fn target_world_name(&self) -> String { + // TODO: naming format!("wasm-rpc-stub-{}", self.source_world_name()) } pub fn target_wit_root(&self) -> PathBuf { - self.target_root.join(naming::wit::WIT_DIR) + self.config.target_root.join(naming::wit::WIT_DIR) } pub fn target_wit_path(&self) -> PathBuf { @@ -152,42 +175,8 @@ impl StubDefinition { ResolvedWitDir::new(&self.target_wit_root()) } - pub fn fix_inlined_owner(&self, typedef: &TypeDef) -> StubTypeOwner { - if self.is_inlined(typedef) { - StubTypeOwner::StubInterface - } else { - StubTypeOwner::Source(typedef.owner) - } - } - - fn is_inlined(&self, typedef: &TypeDef) -> bool { - match &typedef.owner { - TypeOwner::Interface(interface_id) => { - if self.always_inline_types { - if let Some(resolved_owner_interface) = - self.resolve.interfaces.get(*interface_id) - { - if let Some(name) = resolved_owner_interface.name.as_ref() { - self.source_interfaces() - .iter() - .any(|interface| &interface.name == name) - } else { - false - } - } else { - false - } - } else { - false - } - } - TypeOwner::World(_) => true, - TypeOwner::None => false, - } - } - - pub fn source_interfaces(&self) -> &Vec { - self.source_interfaces.get_or_init(|| { + pub fn stub_imported_interfaces(&self) -> &Vec { + self.stub_imported_interfaces.get_or_init(|| { let WorldItemsByType { types, functions, @@ -202,6 +191,148 @@ impl StubDefinition { }) } + pub fn stub_used_type_defs(&self) -> &Vec { + self.stub_used_type_defs.get_or_init(|| { + let imported_type_ids = self + .stub_imported_interfaces() + .iter() + .flat_map(|interface| &interface.used_types) + .cloned() + .collect::>(); + + let imported_type_names = { + let mut imported_type_names = HashMap::>::new(); + for type_id in imported_type_ids { + let type_def = self.resolve.types.get(type_id).unwrap_or_else(|| { + panic!("Imported type not found, type id: {:?}", type_id) + }); + let type_name = type_def + .name + .clone() + .unwrap_or_else(|| panic!("Missing type name, type id: {:?}", type_id)); + + imported_type_names + .entry(type_name) + .or_default() + .insert(type_id); + } + imported_type_names + }; + + imported_type_names + .into_iter() + .flat_map(|(type_name, type_ids)| { + let type_name_is_unique = type_ids.len() == 1; + type_ids.into_iter().map(move |type_id| { + let type_def = self + .resolve + .types + .get(type_id) + .unwrap_or_else(|| { + panic!("Imported type not found, type id: {:?}", type_id) + }) + .clone(); + + let TypeOwner::Interface(interface_id) = type_def.owner else { + panic!( + "Expected interface owner for type, got: {:?}, type name: {:?}", + type_def, type_def.name + ); + }; + + let interface = + self.resolve + .interfaces + .get(interface_id) + .unwrap_or_else(|| { + panic!("Interface not found, interface id: {:?}", interface_id) + }); + + let interface_name = interface.name.clone().unwrap_or_else(|| { + panic!("Missing interface name, interface id: {:?}", interface_id) + }); + + let package = interface.package.map(|package_id| { + self.resolve.packages.get(package_id).unwrap_or_else(|| { + panic!( + "Missing package for interface, package id: {:?}, interface id: {:?}", + package_id, interface_id, + ) + }) + }); + + let interface_identifier = package + .map(|package| package.name.interface_id(&interface_name)) + .unwrap_or(interface_name.clone()); + + let type_name_alias = (!type_name_is_unique).then(|| match &package { + Some(package) => format!( + "{}-{}-{}-{}", + package.name.namespace, + package.name.name, + interface_name, + &type_name + ), + None => format!("{}-{}", interface_name, type_name), + }); + + InterfaceStubTypeDef { + package_id: interface.package, + type_id, + type_def, + interface_identifier, + type_name: type_name.clone(), + type_name_alias, + } + }) + }) + .sorted_by(|a, b| { + let a = (&a.interface_identifier, &a.type_name, &a.type_name_alias); + let b = (&b.interface_identifier, &b.type_name, &b.type_name_alias); + a.cmp(&b) + }) + .collect::>() + }) + } + + pub fn stub_dep_package_ids(&self) -> &HashSet { + self.stub_dep_package_ids.get_or_init(|| { + self.stub_used_type_defs() + .iter() + .flat_map(|type_def| { + let mut package_ids = Vec::::new(); + self.type_def_owner_package_ids(&type_def.type_def, &mut package_ids); + package_ids + }) + .collect() + }) + } + + fn type_def_owner_package_ids(&self, type_def: &TypeDef, package_ids: &mut Vec) { + let package_id = match type_def.owner { + TypeOwner::World(_) => None, + TypeOwner::Interface(interface) => self + .resolve + .interfaces + .get(interface) + .and_then(|interface| interface.package), + TypeOwner::None => None, + }; + + if let Some(package_id) = package_id { + package_ids.push(package_id); + } + + if let TypeDefKind::Type(Type::Id(type_id)) = type_def.kind { + self.type_def_owner_package_ids( + self.resolve.types.get(type_id).unwrap_or_else(|| { + panic!("Type alias target not found, type id: {:?}", type_id) + }), + package_ids, + ); + } + } + fn partition_world_items<'a>( &'a self, world_items: &'a IndexMap, @@ -258,9 +389,9 @@ impl StubDefinition { typ: *typ, }); } - FunctionResultStub::Multi(param_stubs) + FunctionResultStub::Named(param_stubs) } - Results::Anon(single) => FunctionResultStub::Single(*single), + Results::Anon(single) => FunctionResultStub::Anon(*single), }; FunctionStub { @@ -291,12 +422,12 @@ impl StubDefinition { .filter(|function| function.kind == FunctionKind::Freestanding), ); - let (imported_interfaces, _) = self.extract_interface_stubs_from_types(types.into_iter()); + let (used_types, _) = self.extract_interface_stubs_from_types(types.into_iter()); Some(InterfaceStub { - name, + name: name.to_string(), functions, - imports: imported_interfaces, + used_types, global: true, constructor_params: None, static_functions: vec![], @@ -311,7 +442,7 @@ impl StubDefinition { ) -> Vec { let functions = Self::functions_to_stubs(interface.functions.values()); - let (imported_interfaces, resource_interfaces) = self.extract_interface_stubs_from_types( + let (used_types, resource_interfaces) = self.extract_interface_stubs_from_types( interface .types .iter() @@ -323,7 +454,7 @@ impl StubDefinition { interface_stubs.push(InterfaceStub { name, functions, - imports: imported_interfaces, + used_types, global: false, constructor_params: None, static_functions: vec![], @@ -337,8 +468,8 @@ impl StubDefinition { fn extract_interface_stubs_from_types( &self, types: impl Iterator, - ) -> (Vec, Vec) { - let mut imported_interfaces = Vec::::new(); + ) -> (Vec, Vec) { + let mut used_types = Vec::::new(); let mut resource_interfaces = Vec::::new(); for (type_name, type_id) in types { @@ -362,43 +493,12 @@ impl StubDefinition { type_id, )) } else { - imported_interfaces.push(self.type_def_to_stub( - owner_interface, - type_name, - type_def.clone(), - )); + used_types.push(type_id); } } } - (imported_interfaces, resource_interfaces) - } - - fn type_def_to_stub( - &self, - owner_interface: &Interface, - type_name: String, - type_def: TypeDef, - ) -> InterfaceStubTypeDef { - let package = owner_interface - .package - .and_then(|id| self.resolve.packages.get(id)); - - let interface_name = owner_interface - .name - .clone() - .unwrap_or_else(|| panic!("Failed to get owner interface name")); - - let interface_path = package - .map(|p| p.name.interface_id(&interface_name)) - .unwrap_or(interface_name); - - InterfaceStubTypeDef { - name: type_name, - path: interface_path, - package_name: package.map(|p| p.name.clone()), - type_def, - } + (used_types, resource_interfaces) } fn resource_interface_stub( @@ -417,7 +517,7 @@ impl StubDefinition { let function_stubs_by_kind = |kind: FunctionKind| Self::functions_to_stubs(functions_by_kind(kind)); - let (imported_interfaces, _) = self.extract_interface_stubs_from_types( + let (used_types, _) = self.extract_interface_stubs_from_types( owner_interface .types .iter() @@ -439,7 +539,7 @@ impl StubDefinition { InterfaceStub { name: type_name, functions: function_stubs_by_kind(FunctionKind::Method(type_id)), - imports: imported_interfaces, + used_types, global: false, constructor_params, static_functions: function_stubs_by_kind(FunctionKind::Static(type_id)), @@ -482,6 +582,14 @@ impl StubDefinition { .get(type_id) .ok_or_else(|| anyhow!("failed to get type by id: {:?}", type_id)) } + + pub fn get_stub_used_type_alias(&self, type_id: TypeId) -> Option<&str> { + // TODO: hash map + self.stub_used_type_defs() + .iter() + .find(|type_def| type_def.type_id == type_id) + .and_then(|type_def| type_def.type_name_alias.as_deref()) + } } struct WorldItemsByType<'a> { @@ -496,30 +604,40 @@ pub struct InterfaceStub { pub constructor_params: Option>, pub functions: Vec, pub static_functions: Vec, - pub imports: Vec, + pub used_types: Vec, pub global: bool, pub owner_interface: Option, } +impl InterfaceStub { + // The returned bool is true if the function is static + pub fn all_functions(&self) -> impl Iterator { + self.static_functions + .iter() + .map(|f| (f, true)) + .chain(self.functions.iter().map(|f| (f, false))) + } +} + impl InterfaceStub { pub fn is_resource(&self) -> bool { self.constructor_params.is_some() } - pub fn interface_name(&self) -> Option { + pub fn interface_name(&self) -> Option<&str> { if self.global { None } else { match &self.owner_interface { - Some(iface) => Some(iface.clone()), - None => Some(self.name.clone()), + Some(iface) => Some(iface), + None => Some(&self.name), } } } - pub fn resource_name(&self) -> Option { + pub fn resource_name(&self) -> Option<&str> { if self.is_resource() { - Some(self.name.clone()) + Some(&self.name) } else { None } @@ -528,24 +646,17 @@ impl InterfaceStub { #[derive(Debug, Clone, PartialEq)] pub struct InterfaceStubTypeDef { - pub name: String, - pub path: String, - pub package_name: Option, + pub package_id: Option, + pub type_id: TypeId, pub type_def: TypeDef, + pub interface_identifier: String, + pub type_name: String, + pub type_name_alias: Option, } -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub struct InterfaceStubImport { - pub name: String, - pub path: String, -} - -impl From<&InterfaceStubTypeDef> for InterfaceStubImport { - fn from(value: &InterfaceStubTypeDef) -> Self { - Self { - name: value.name.clone(), - path: value.path.clone(), - } +impl InterfaceStubTypeDef { + pub fn stub_type_name(&self) -> &str { + self.type_name_alias.as_deref().unwrap_or(&self.type_name) } } @@ -558,27 +669,15 @@ pub struct FunctionStub { impl FunctionStub { pub fn as_method(&self) -> Option { - self.name.strip_prefix("[method]").and_then(|method_name| { - let parts = method_name.split('.').collect::>(); - if parts.len() != 2 { - None - } else { - Some(FunctionStub { - name: parts[1].to_string(), - params: self - .params - .iter() - .filter(|p| p.name != "self") - .cloned() - .collect(), - results: self.results.clone(), - }) - } - }) + self.as_function_stub_without_prefix("[method]") } pub fn as_static(&self) -> Option { - self.name.strip_prefix("[static]").and_then(|method_name| { + self.as_function_stub_without_prefix("[static]") + } + + fn as_function_stub_without_prefix(&self, prefix: &str) -> Option { + self.name.strip_prefix(prefix).and_then(|method_name| { let parts = method_name.split('.').collect::>(); if parts.len() != 2 { None @@ -614,23 +713,17 @@ pub struct FunctionParamStub { #[derive(Debug, Clone)] pub enum FunctionResultStub { - Single(Type), - Multi(Vec), + Anon(Type), + Named(Vec), SelfType, } impl FunctionResultStub { pub fn is_empty(&self) -> bool { match self { - FunctionResultStub::Single(_) => false, - FunctionResultStub::Multi(params) => params.is_empty(), + FunctionResultStub::Anon(_) => false, + FunctionResultStub::Named(params) => params.is_empty(), FunctionResultStub::SelfType => false, } } } - -#[derive(Debug, Clone)] -pub enum StubTypeOwner { - StubInterface, - Source(TypeOwner), -} diff --git a/wasm-rpc-stubgen/src/model/validation.rs b/wasm-rpc-stubgen/src/validation.rs similarity index 75% rename from wasm-rpc-stubgen/src/model/validation.rs rename to wasm-rpc-stubgen/src/validation.rs index a417d03f..c158f63f 100644 --- a/wasm-rpc-stubgen/src/model/validation.rs +++ b/wasm-rpc-stubgen/src/validation.rs @@ -1,5 +1,6 @@ +use crate::log::LogColorize; use itertools::Itertools; -use std::fmt::Display; +use std::fmt::Debug; pub struct ValidationContext { pub name: &'static str, @@ -21,7 +22,7 @@ impl ValidatedResult { } } - pub fn from_result(result: Result) -> Self { + pub fn from_result(result: Result) -> Self { ValidatedResult::Ok(result).flatten() } @@ -47,7 +48,7 @@ impl ValidatedResult { pub fn combine(self, u: ValidatedResult, combine: C) -> ValidatedResult where - C: Fn(T, U) -> V, + C: FnOnce(T, U) -> V, { let (t, mut t_warns, mut t_errors) = self.into_product(); let (u, u_warns, u_errors) = u.into_product(); @@ -119,16 +120,16 @@ impl ValidatedResult> { pub fn flatten(self) -> ValidatedResult where - E: Display, + E: Debug, { match self { ValidatedResult::Ok(value) => match value { Ok(value) => ValidatedResult::Ok(value), - Err(err) => ValidatedResult::WarnsAndErrors(vec![], vec![format!("{}", err)]), + Err(err) => ValidatedResult::WarnsAndErrors(vec![], vec![format!("{:?}", err)]), }, ValidatedResult::OkWithWarns(value, warns) => match value { Ok(value) => ValidatedResult::OkWithWarns(value, warns), - Err(err) => ValidatedResult::WarnsAndErrors(warns, vec![format!("{}", err)]), + Err(err) => ValidatedResult::WarnsAndErrors(warns, vec![format!("{:?}", err)]), }, ValidatedResult::WarnsAndErrors(warns, errors) => { ValidatedResult::WarnsAndErrors(warns, errors) @@ -188,33 +189,85 @@ impl ValidationBuilder { _ = self.context.pop(); } + fn format(&mut self, message: String) -> String { + let multiline = message.contains("\n"); + + let message = { + if multiline { + message.lines().map(|l| format!(" {}", l)).join("\n") + } else { + message + } + }; + + let context = { + if self.context.is_empty() { + "".to_string() + } else if multiline { + format!( + "{}{}", + if message.ends_with("\n") { + "\n " + } else { + "\n\n " + }, + self.context + .iter() + .map(|c| format!("{}: {}", c.name, c.value.log_color_highlight())) + .join("\n") + ) + } else { + format!( + ", {}", + self.context + .iter() + .map(|c| format!("{}: {}", c.name, c.value.log_color_highlight())) + .join(", ") + ) + } + }; + + format!( + "{}{}{}", + if multiline && !message.starts_with("\n") { + "\n" + } else { + "" + }, + message, + context + ) + } + pub fn add_error(&mut self, error: String) { - self.errors.push(format!("{}{}", error, self.context(),)); + let error = self.format(error); + self.errors.push(error); } pub fn add_warn(&mut self, warn: String) { - self.warns.push(format!("{}{}", warn, self.context(),)); + let warn = self.format(warn); + self.warns.push(warn); } - pub fn add_errors(&mut self, elems: C, context_and_error: F) + pub fn add_errors(&mut self, elems: E, context_and_error: CE) where - C: IntoIterator, - F: Fn(T) -> Option<(Vec<(&'static str, String)>, String)>, + E: IntoIterator, + CE: Fn(T) -> Option<(Vec<(&'static str, String)>, String)>, { - self.add(elems, context_and_error, Self::add_error); + self.add_all(elems, context_and_error, Self::add_error); } - pub fn add_warns(&mut self, elems: C, context_and_error: F) + pub fn add_warns(&mut self, elems: E, context_and_error: CE) where - C: IntoIterator, - F: Fn(T) -> Option<(Vec<(&'static str, String)>, String)>, + E: IntoIterator, + CE: Fn(T) -> Option<(Vec<(&'static str, String)>, String)>, { - self.add(elems, context_and_error, Self::add_warn); + self.add_all(elems, context_and_error, Self::add_warn); } - pub fn add(&mut self, elems: C, context_and_error: CE, add: A) + fn add_all(&mut self, elems: E, context_and_error: CE, add: A) where - C: IntoIterator, + E: IntoIterator, CE: Fn(T) -> Option<(Vec<(&'static str, String)>, String)>, A: Fn(&mut Self, String), { @@ -247,20 +300,6 @@ impl ValidationBuilder { ValidatedResult::WarnsAndErrors(self.warns, self.errors) } } - - fn context(&self) -> String { - if self.context.is_empty() { - "".to_string() - } else { - format!( - ", {}", - self.context - .iter() - .map(|c| format!("{}: {}", c.name, c.value)) - .join(", ") - ) - } - } } impl Default for ValidationBuilder { diff --git a/wasm-rpc-stubgen/src/wit.rs b/wasm-rpc-stubgen/src/wit.rs deleted file mode 100644 index f51bc0e4..00000000 --- a/wasm-rpc-stubgen/src/wit.rs +++ /dev/null @@ -1,659 +0,0 @@ -// Copyright 2024 Golem Cloud -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::commands::log::{log_action, log_warn_action}; -use crate::fs::{copy, copy_transformed, get_file_name}; -use crate::stub::{ - FunctionParamStub, FunctionResultStub, FunctionStub, InterfaceStub, InterfaceStubImport, - InterfaceStubTypeDef, StubDefinition, -}; -use crate::{naming, WasmRpcOverride}; -use anyhow::{anyhow, bail}; -use indexmap::IndexMap; -use regex::Regex; -use std::fmt::Write; -use std::fs; -use std::path::Path; -use wit_parser::{ - Enum, Field, Flags, Handle, PackageName, Result_, Tuple, Type, TypeDef, TypeDefKind, Variant, -}; - -pub fn generate_stub_wit_to_target(def: &StubDefinition) -> anyhow::Result<()> { - log_action( - "Generating", - format!("stub WIT to {}", def.target_wit_path().to_string_lossy()), - ); - - let out = generate_stub_wit_from_stub_def(def)?; - fs::create_dir_all(def.target_wit_root())?; - fs::write(def.target_wit_path(), out)?; - Ok(()) -} - -pub fn generate_stub_wit_from_wit_dir( - source_wit_root: &Path, - inline_root_types: bool, -) -> anyhow::Result { - generate_stub_wit_from_stub_def(&StubDefinition::new( - source_wit_root, - source_wit_root, // Not used - &None, // Not used - "0.0.1", // Not used - &WasmRpcOverride::default(), // Not used - inline_root_types, - )?) -} - -pub fn generate_stub_wit_from_stub_def(def: &StubDefinition) -> anyhow::Result { - let world = def.source_world(); - - let mut out = String::new(); - - writeln!(out, "package {}-stub;", def.source_package_name)?; - writeln!(out)?; - writeln!(out, "interface stub-{} {{", world.name)?; - - let all_imports = def - .source_interfaces() - .iter() - .flat_map(|i| i.imports.iter().map(|i| (InterfaceStubImport::from(i), i))) - .collect::>(); - - // Renaming the mandatory imports to avoid collisions with types coming from the stubbed package - writeln!(out, " use golem:rpc/types@0.1.0.{{uri as golem-rpc-uri}};")?; - writeln!( - out, - " use wasi:io/poll@0.2.0.{{pollable as wasi-io-pollable}};" - )?; - - if def.always_inline_types { - let mut inline_types: Vec = vec![]; - - for (import, type_def_info) in all_imports { - match &type_def_info.package_name { - Some(package) if package == &def.source_package_name => { - inline_types.push(type_def_info.clone()); - } - _ => writeln!(out, " use {}.{{{}}};", import.path, import.name)?, - } - } - - writeln!(out)?; - - for typ in inline_types { - write_type_def(&mut out, &typ.type_def, typ.name.as_str(), def)?; - } - } else { - for (import, _) in all_imports { - writeln!(out, " use {}.{{{}}};", import.path, import.name)?; - } - writeln!(out)?; - } - - // Generating async return types - for interface in def.source_interfaces() { - for function in &interface.functions { - if !function.results.is_empty() { - write_async_return_type(&mut out, function, interface, def)?; - } - } - for function in &interface.static_functions { - if !function.results.is_empty() { - write_async_return_type(&mut out, function, interface, def)?; - } - } - } - - // Generating function definitions - for interface in def.source_interfaces() { - writeln!(out, " resource {} {{", &interface.name)?; - match &interface.constructor_params { - None => { - writeln!(out, " constructor(location: golem-rpc-uri);")?; - } - Some(params) => { - write!(out, " constructor(location: golem-rpc-uri")?; - if !params.is_empty() { - write!(out, ", ")?; - } - write_param_list(&mut out, def, params)?; - writeln!(out, ");")?; - } - } - for function in &interface.functions { - write_function_definition(&mut out, function, false, interface, def)?; - } - for function in &interface.static_functions { - write_function_definition(&mut out, function, true, interface, def)?; - } - writeln!(out, " }}")?; - writeln!(out)?; - } - - writeln!(out, "}}")?; - writeln!(out)?; - - writeln!(out, "world {} {{", def.target_world_name())?; - writeln!(out, " export stub-{};", world.name)?; - writeln!(out, "}}")?; - - Ok(out) -} - -fn write_function_definition( - out: &mut String, - function: &FunctionStub, - is_static: bool, - owner: &InterfaceStub, - def: &StubDefinition, -) -> anyhow::Result<()> { - let func = if is_static { "static_func" } else { "func" }; - if !function.results.is_empty() { - // Write the blocking function - write!(out, " blocking-{}: {func}(", function.name)?; - write_param_list(out, def, &function.params)?; - write!(out, ")")?; - write!(out, " -> ")?; - write_function_result_type(out, function, def)?; - writeln!(out, ";")?; - // Write the non-blocking function - write!(out, " {}: {func}(", function.name)?; - write_param_list(out, def, &function.params)?; - write!(out, ")")?; - write!(out, " -> {}", function.async_result_type(owner))?; - } else { - // Write the blocking function - write!(out, " blocking-{}: {func}(", function.name)?; - write_param_list(out, def, &function.params)?; - write!(out, ")")?; - writeln!(out, ";")?; - - // Write the non-blocking function - write!(out, " {}: {func}(", function.name)?; - write_param_list(out, def, &function.params)?; - write!(out, ")")?; - } - writeln!(out, ";")?; - Ok(()) -} - -fn write_async_return_type( - out: &mut String, - function: &FunctionStub, - owner: &InterfaceStub, - def: &StubDefinition, -) -> anyhow::Result<()> { - writeln!(out, " resource {} {{", function.async_result_type(owner))?; - writeln!(out, " subscribe: func() -> wasi-io-pollable;")?; - write!(out, " get: func() -> option<")?; - write_function_result_type(out, function, def)?; - writeln!(out, ">;")?; - writeln!(out, " }}")?; - Ok(()) -} - -fn write_function_result_type( - out: &mut String, - function: &FunctionStub, - def: &StubDefinition, -) -> anyhow::Result<()> { - match &function.results { - FunctionResultStub::Single(typ) => { - write!(out, "{}", typ.wit_type_string(def)?)?; - } - FunctionResultStub::Multi(params) => { - write!(out, "(")?; - write_param_list(out, def, params)?; - write!(out, ")")?; - } - FunctionResultStub::SelfType => { - return Err(anyhow!("Unexpected return type in wit generator")); - } - } - Ok(()) -} - -fn write_type_def( - out: &mut String, - typ: &TypeDef, - typ_name: &str, - def: &StubDefinition, -) -> anyhow::Result<()> { - let typ_kind = &typ.kind; - let kind_str = typ_kind.as_str(); - - match typ_kind { - TypeDefKind::Record(record) => { - write!(out, " {}", kind_str)?; - write!(out, " {}", typ_name)?; - - write_record(out, &record.fields, def)?; - } - - TypeDefKind::Flags(flags) => { - write!(out, " {}", kind_str)?; - write!(out, " {}", typ_name)?; - - write_flags(out, flags)?; - } - TypeDefKind::Tuple(tuple) => { - if let Some(name) = &typ.name { - write!(out, " type {} =", name)?; - write!(out, " {}", kind_str)?; - write_tuple(out, tuple, def)?; - writeln!(out, ";")?; - } else { - write!(out, " {}", kind_str)?; - write_tuple(out, tuple, def)?; - } - } - TypeDefKind::Variant(variant) => { - write!(out, " {}", kind_str)?; - write!(out, " {}", typ_name)?; - write_variant(out, variant, def)?; - } - TypeDefKind::Enum(enum_ty) => { - write!(out, " {}", kind_str)?; - write!(out, " {}", typ_name)?; - write_enum(out, enum_ty)?; - } - TypeDefKind::Option(option) => { - if let Some(name) = &typ.name { - write!(out, " type {} =", name)?; - write!(out, " {}", kind_str)?; - write_option(out, option, def)?; - writeln!(out, ";")?; - } else { - write!(out, " {}", kind_str)?; - write_option(out, option, def)?; - } - } - - TypeDefKind::Result(result) => { - if let Some(name) = &typ.name { - write!(out, " type {} =", name)?; - write!(out, " {}", kind_str)?; - write_result(out, result, def)?; - writeln!(out, ";")?; - } else { - write!(out, " {}", kind_str)?; - write_result(out, result, def)?; - } - } - - TypeDefKind::List(list_typ) => { - if let Some(name) = &typ.name { - write!(out, " type {} =", name)?; - write!(out, " {}", kind_str)?; - write!(out, " {}", list_typ.wit_type_string(def)?)?; - writeln!(out, ";")?; - } else { - write!(out, " {}", kind_str)?; - write!(out, " {}", list_typ.wit_type_string(def)?)?; - } - } - TypeDefKind::Future(_) => {} - TypeDefKind::Stream(_) => {} - TypeDefKind::Type(aliased_typ) => match aliased_typ { - Type::Id(type_id) => { - let aliased_type_def = def.get_type_def(*type_id)?; - if aliased_type_def.owner == typ.owner { - // type alias to type defined in the same interface - write!(out, " {} ", kind_str)?; - write!(out, "{}", typ_name)?; - write!(out, " =")?; - write!(out, " {}", aliased_typ.wit_type_string(def)?)?; - writeln!(out, ";")?; - } else { - // import from another interface - write_type_def(out, aliased_type_def, typ_name, def)?; - } - } - _ => { - // type alias to primitive - write!(out, " {} ", kind_str)?; - write!(out, "{}", typ_name)?; - write!(out, " =")?; - write!(out, " {}", aliased_typ.wit_type_string(def)?)?; - writeln!(out, ";")?; - } - }, - TypeDefKind::Unknown => { - write!(out, " {}", kind_str)?; - } - TypeDefKind::Resource => { - write!(out, " {}", kind_str)?; - } - TypeDefKind::Handle(handle) => { - write!(out, " {}", kind_str)?; - write_handle(out, handle, def)?; - } - } - - Ok(()) -} - -// https://github.com/WebAssembly/component-model/blob/main/design/mvp/WIT.md#wit-types -fn write_handle(out: &mut String, handle: &Handle, def: &StubDefinition) -> anyhow::Result<()> { - match handle { - Handle::Own(type_id) => { - write!(out, "{}", Type::Id(*type_id).wit_type_string(def)?)?; - } - Handle::Borrow(type_id) => { - write!(out, " borrow<")?; - write!(out, "{}", Type::Id(*type_id).wit_type_string(def)?)?; - write!(out, ">")?; - } - } - - Ok(()) -} - -// https://github.com/WebAssembly/component-model/blob/main/design/mvp/WIT.md#wit-types -fn write_result(out: &mut String, result: &Result_, def: &StubDefinition) -> anyhow::Result<()> { - match (result.ok, result.err) { - (Some(ok), Some(err)) => { - write!(out, "<")?; - write!(out, "{}", ok.wit_type_string(def)?)?; - write!(out, ", ")?; - write!(out, "{}", err.wit_type_string(def)?)?; - write!(out, ">")?; - } - (Some(ok), None) => { - write!(out, "<")?; - write!(out, "{}", ok.wit_type_string(def)?)?; - write!(out, ">")?; - } - (None, Some(err)) => { - write!(out, "<_, ")?; - write!(out, "{}", err.wit_type_string(def)?)?; - write!(out, ">")?; - } - (None, None) => {} - } - - Ok(()) -} - -fn write_option(out: &mut String, option: &Type, def: &StubDefinition) -> anyhow::Result<()> { - write!(out, "<")?; - write!(out, "{}", option.wit_type_string(def)?)?; - write!(out, ">")?; - Ok(()) -} - -fn write_variant(out: &mut String, variant: &Variant, def: &StubDefinition) -> anyhow::Result<()> { - let cases_length = variant.cases.len(); - write!(out, " {{")?; - writeln!(out)?; - - for (idx, case) in variant.cases.iter().enumerate() { - write!(out, " {}", case.name)?; - - if let Some(ty) = case.ty { - write!(out, "(")?; - write!(out, "{}", ty.wit_type_string(def)?)?; - write!(out, ")")?; - } - - if idx < cases_length - 1 { - writeln!(out, ", ")?; - } - } - - writeln!(out, " }}")?; - Ok(()) -} - -fn write_enum(out: &mut String, enum_ty: &Enum) -> anyhow::Result<()> { - let length = enum_ty.cases.len(); - write!(out, " {{")?; - writeln!(out)?; - for (idx, case) in enum_ty.cases.iter().enumerate() { - write!(out, " {}", case.name)?; - if idx < length - 1 { - writeln!(out, ",")?; - } - writeln!(out)?; - } - writeln!(out, " }}")?; - Ok(()) -} - -fn write_tuple(out: &mut String, tuple: &Tuple, def: &StubDefinition) -> anyhow::Result<()> { - let tuple_length = tuple.types.len(); - write!(out, " <")?; - for (idx, typ) in tuple.types.iter().enumerate() { - write!(out, "{}", typ.wit_type_string(def)?)?; - if idx < tuple_length - 1 { - write!(out, ", ")?; - } - } - write!(out, " >")?; - Ok(()) -} - -fn write_record(out: &mut String, fields: &[Field], def: &StubDefinition) -> anyhow::Result<()> { - write!(out, " {{")?; - writeln!(out)?; - - for (idx, field) in fields.iter().enumerate() { - write!( - out, - " {}: {}", - field.name, - field.ty.wit_type_string(def)? - )?; - if idx < fields.len() - 1 { - write!(out, ", ")?; - } - } - - writeln!(out)?; - writeln!(out, " }}")?; - - Ok(()) -} - -fn write_flags(out: &mut String, flags: &Flags) -> anyhow::Result<()> { - write!(out, " {{")?; - writeln!(out)?; - let flags_len = flags.flags.len(); - for (idx, flag) in flags.flags.iter().enumerate() { - write!(out, " {}", flag.name)?; - if idx < flags_len - 1 { - writeln!(out, ",")?; - } - writeln!(out)?; - } - writeln!(out, " }}")?; - Ok(()) -} - -fn write_param_list( - out: &mut String, - def: &StubDefinition, - params: &[FunctionParamStub], -) -> anyhow::Result<()> { - for (idx, param) in params.iter().enumerate() { - write!(out, "{}: {}", param.name, param.typ.wit_type_string(def)?)?; - if idx < params.len() - 1 { - write!(out, ", ")?; - } - } - Ok(()) -} - -pub fn copy_wit_dependencies(def: &StubDefinition) -> anyhow::Result<()> { - let stub_package_name = def.stub_package_name(); - let remove_stub_imports = import_remover(&stub_package_name); - - let target_wit_root = def.target_wit_root(); - let target_deps = target_wit_root.join(naming::wit::DEPS_DIR); - - for (package, (_, sources)) in def.packages_with_wit_sources() { - if package.name == stub_package_name { - log_warn_action("Skipping", format!("package {}", package.name)); - continue; - } - - let is_source_package = package.name == def.source_package_name; - - log_action("Copying", format!("source package {}", package.name)); - for source in sources { - if is_source_package { - let dest = target_deps - .join(naming::wit::package_dep_dir_name(&def.source_package_name)) - .join(get_file_name(source)?); - log_action( - " Copying", - format!( - "(with source imports removed) {} to {}", - source.to_string_lossy(), - dest.to_string_lossy() - ), - ); - copy_transformed(source, &dest, &remove_stub_imports)?; - } else { - let relative = source.strip_prefix(&def.source_wit_root)?; - let dest = target_wit_root.join(relative); - log_action( - " Copying", - format!("{} to {}", source.to_string_lossy(), dest.to_string_lossy()), - ); - copy(source, &dest)?; - } - } - } - - write_embedded_source( - &target_deps.join("wasm-rpc"), - "wasm-rpc.wit", - golem_wasm_rpc::WASM_RPC_WIT, - )?; - - write_embedded_source( - &target_deps.join("io"), - "poll.wit", - golem_wasm_rpc::WASI_POLL_WIT, - )?; - - Ok(()) -} - -fn write_embedded_source(target_dir: &Path, file_name: &str, content: &str) -> anyhow::Result<()> { - fs::create_dir_all(target_dir)?; - - log_action( - "Writing", - format!("{} to {}", file_name, target_dir.to_string_lossy()), - ); - - fs::write(target_dir.join(file_name), content)?; - - Ok(()) -} - -trait TypeExtensions { - fn wit_type_string(&self, stub_definition: &StubDefinition) -> anyhow::Result; -} - -impl TypeExtensions for Type { - fn wit_type_string(&self, stub_definition: &StubDefinition) -> anyhow::Result { - match self { - Type::Bool => Ok("bool".to_string()), - Type::U8 => Ok("u8".to_string()), - Type::U16 => Ok("u16".to_string()), - Type::U32 => Ok("u32".to_string()), - Type::U64 => Ok("u64".to_string()), - Type::S8 => Ok("s8".to_string()), - Type::S16 => Ok("s16".to_string()), - Type::S32 => Ok("s32".to_string()), - Type::S64 => Ok("s64".to_string()), - Type::F32 => Ok("f32".to_string()), - Type::F64 => Ok("f64".to_string()), - Type::Char => Ok("char".to_string()), - Type::String => Ok("string".to_string()), - Type::Id(type_id) => { - let typ = stub_definition.get_type_def(*type_id)?; - match &typ.kind { - TypeDefKind::Option(inner) => Ok(format!( - "option<{}>", - inner.wit_type_string(stub_definition)? - )), - TypeDefKind::List(inner) => { - Ok(format!("list<{}>", inner.wit_type_string(stub_definition)?)) - } - TypeDefKind::Tuple(tuple) => { - let types = tuple - .types - .iter() - .map(|t| t.wit_type_string(stub_definition)) - .collect::>>()?; - Ok(format!("tuple<{}>", types.join(", "))) - } - TypeDefKind::Result(result) => match (&result.ok, &result.err) { - (Some(ok), Some(err)) => { - let ok = ok.wit_type_string(stub_definition)?; - let err = err.wit_type_string(stub_definition)?; - Ok(format!("result<{}, {}>", ok, err)) - } - (Some(ok), None) => { - let ok = ok.wit_type_string(stub_definition)?; - Ok(format!("result<{}>", ok)) - } - (None, Some(err)) => { - let err = err.wit_type_string(stub_definition)?; - Ok(format!("result<_, {}>", err)) - } - (None, None) => { - bail!("result type has no ok or err types") - } - }, - TypeDefKind::Handle(handle) => match handle { - Handle::Own(type_id) => Type::Id(*type_id).wit_type_string(stub_definition), - Handle::Borrow(type_id) => Ok(format!( - "borrow<{}>", - Type::Id(*type_id).wit_type_string(stub_definition)? - )), - }, - _ => { - let name = typ - .name - .clone() - .ok_or(anyhow!("wit_type_string: type has no name"))?; - Ok(name) - } - } - } - } - } -} - -pub fn import_remover(package_name: &PackageName) -> impl Fn(String) -> anyhow::Result { - let pattern_import_stub_package_name = Regex::new( - format!( - r"import\s+{}(/[^;]*)?;", - regex::escape(&package_name.to_string()) - ) - .as_str(), - ) - .unwrap_or_else(|err| panic!("Failed to compile package import regex: {}", err)); - - move |src: String| -> anyhow::Result { - Ok(pattern_import_stub_package_name - .replace_all(&src, "") - .to_string()) - } -} diff --git a/wasm-rpc-stubgen/src/wit_encode.rs b/wasm-rpc-stubgen/src/wit_encode.rs new file mode 100644 index 00000000..f9b66a26 --- /dev/null +++ b/wasm-rpc-stubgen/src/wit_encode.rs @@ -0,0 +1,39 @@ +use anyhow::anyhow; +use indexmap::IndexMap; +use wit_encoder::{packages_from_parsed, Package}; + +pub struct EncodedWitDir { + encoded_packages_by_parser_id: IndexMap, +} + +impl EncodedWitDir { + pub fn new(resolve: &wit_parser::Resolve) -> anyhow::Result { + let mut encoded_packages_by_parser_id = IndexMap::::new(); + + for package in packages_from_parsed(resolve) { + let package_name = package.name(); + let package_name = wit_parser::PackageName { + namespace: package_name.namespace().to_string(), + name: package_name.name().to_string(), + version: package_name.version().cloned(), + }; + + let package_id = resolve + .package_names + .get(&package_name) + .cloned() + .ok_or_else(|| anyhow!("Failed to get package by name: {}", package.name()))?; + encoded_packages_by_parser_id.insert(package_id, package); + } + + Ok(Self { + encoded_packages_by_parser_id, + }) + } + + pub fn package(&mut self, package_id: wit_parser::PackageId) -> anyhow::Result<&mut Package> { + self.encoded_packages_by_parser_id + .get_mut(&package_id) + .ok_or_else(|| anyhow!("Failed to get encoded package by id: {:?}", package_id)) + } +} diff --git a/wasm-rpc-stubgen/src/wit_generate.rs b/wasm-rpc-stubgen/src/wit_generate.rs new file mode 100644 index 00000000..05202bd5 --- /dev/null +++ b/wasm-rpc-stubgen/src/wit_generate.rs @@ -0,0 +1,847 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::fs::{OverwriteSafeAction, OverwriteSafeActions, PathExtra}; +use crate::log::{log_action, log_action_plan, log_warn_action, LogColorize, LogIndent}; +use crate::naming::wit::package_dep_dir_name_from_encoder; +use crate::stub::{ + FunctionParamStub, FunctionResultStub, FunctionStub, InterfaceStub, StubDefinition, +}; +use crate::wit_encode::EncodedWitDir; +use crate::wit_resolve::ResolvedWitDir; +use crate::{cargo, fs, naming}; +use anyhow::{anyhow, bail, Context}; +use itertools::Itertools; +use std::collections::{BTreeMap, HashSet}; +use std::path::{Path, PathBuf}; +use wit_encoder::{ + Ident, Interface, Package, PackageItem, PackageName, Params, ResourceFunc, Results, + StandaloneFunc, Type, TypeDef, World, WorldItem, +}; +use wit_parser::PackageId; + +pub fn generate_stub_wit_to_target(def: &StubDefinition) -> anyhow::Result<()> { + log_action( + "Generating", + format!( + "stub WIT to {}", + def.target_wit_path().log_color_highlight() + ), + ); + + let out = generate_stub_wit_from_stub_def(def)?; + fs::create_dir_all(def.target_wit_root())?; + fs::write(def.target_wit_path(), out)?; + Ok(()) +} + +pub fn generate_stub_wit_from_stub_def(def: &StubDefinition) -> anyhow::Result { + Ok(generate_stub_package_from_stub_def(def)?.to_string()) +} + +pub fn generate_stub_package_from_stub_def(def: &StubDefinition) -> anyhow::Result { + let mut package = Package::new(PackageName::new( + def.source_package_name.namespace.clone(), + format!("{}-stub", def.source_package_name.name), + def.source_package_name.version.clone(), + )); + + let interface_identifier = def.target_interface_name(); + + // Stub interface + { + let mut stub_interface = Interface::new(interface_identifier.clone()); + + // Common used types + stub_interface.use_type( + "golem:rpc/types@0.1.0", + "uri", + Some(Ident::new("golem-rpc-uri")), + ); + stub_interface.use_type( + "wasi:io/poll@0.2.0", + "pollable", + Some(Ident::new("wasi-io-pollable")), + ); + + // Used or inlined type defs + for type_def in def.stub_used_type_defs() { + stub_interface.use_type( + type_def.interface_identifier.clone(), + type_def.type_name.clone(), + type_def.type_name_alias.clone().map(Ident::from), + ); + } + + // Async return types + for interface in def.stub_imported_interfaces() { + for (function, _) in interface.all_functions() { + if !function.results.is_empty() { + add_async_return_type(def, &mut stub_interface, function, interface)?; + } + } + } + + // Function definitions + for interface in def.stub_imported_interfaces() { + let mut stub_functions = Vec::::new(); + + // Constructor + { + let mut constructor = ResourceFunc::constructor(); + let mut params = match &interface.constructor_params { + Some(constructor_params) => constructor_params.to_encoder(def)?, + None => Params::empty(), + }; + params.items_mut().insert( + 0, + ( + Ident::new("location"), + Type::Named(Ident::new("golem-rpc-uri")), + ), + ); + constructor.set_params(params); + stub_functions.push(constructor); + } + + // Functions + for (function, is_static) in interface.all_functions() { + // Blocking + { + let mut blocking_function = { + let function_name = format!("blocking-{}", function.name.clone()); + if is_static { + ResourceFunc::static_(function_name) + } else { + ResourceFunc::method(function_name) + } + }; + blocking_function.set_params(function.params.to_encoder(def)?); + if !function.results.is_empty() { + blocking_function.set_results(function.results.to_encoder(def)?); + } + stub_functions.push(blocking_function); + } + + // Async + { + let mut async_function = { + if is_static { + ResourceFunc::static_(function.name.clone()) + } else { + ResourceFunc::method(function.name.clone()) + } + }; + async_function.set_params(function.params.to_encoder(def)?); + if !function.results.is_empty() { + async_function.set_results(Results::Anon(Type::Named(Ident::new( + function.async_result_type(interface), + )))); + } + stub_functions.push(async_function); + } + } + + stub_interface.type_def(TypeDef::resource(interface.name.clone(), stub_functions)); + } + + package.interface(stub_interface); + } + + // Stub world + { + let mut stub_world = World::new(def.target_world_name()); + stub_world.named_interface_export(interface_identifier); + package.world(stub_world); + } + + Ok(package) +} + +fn add_async_return_type( + def: &StubDefinition, + stub_interface: &mut Interface, + function: &FunctionStub, + owner_interface: &InterfaceStub, +) -> anyhow::Result<()> { + let context = || { + anyhow!( + "Failed to add async return type to stub, stub interface: {}, owner interface: {}, function: {:?}", stub_interface.name(), owner_interface.name, function.name) + }; + + stub_interface.type_def(TypeDef::resource( + function.async_result_type(owner_interface), + [ + { + let mut subscribe = ResourceFunc::method("subscribe"); + subscribe.set_results(Results::Anon(Type::Named(Ident::new("wasi-io-pollable")))); + subscribe + }, + { + let mut get = ResourceFunc::method("get"); + match &function.results { + FunctionResultStub::Anon(typ) => { + get.set_results(Results::Anon(Type::option( + typ.to_encoder(def).with_context(context)?, + ))); + } + FunctionResultStub::Named(params) => { + Err(anyhow!( + "Named parameters are not supported as async stub result, params: {:?}", + params + )) + .with_context(context)?; + } + FunctionResultStub::SelfType => { + Err(anyhow!("Unexpected self return type")).with_context(context)?; + } + } + get + }, + ], + )); + + Ok(()) +} + +pub fn add_dependencies_to_stub_wit_dir(def: &StubDefinition) -> anyhow::Result<()> { + log_action( + "Adding", + format!( + "WIT dependencies from {} to {}", + def.config.source_wit_root.log_color_highlight(), + def.config.target_root.log_color_highlight(), + ), + ); + + let _indent = LogIndent::new(); + + let stub_dep_packages = def.stub_dep_package_ids(); + + let target_wit_root = def.target_wit_root(); + let target_deps = target_wit_root.join(naming::wit::DEPS_DIR); + + for (package_id, package, package_sources) in def.packages_with_wit_sources() { + if !stub_dep_packages.contains(&package_id) || package_id == def.source_package_id { + log_warn_action( + "Skipping", + format!( + "package dependency {}", + package.name.to_string().log_color_highlight() + ), + ); + continue; + } + + log_action( + "Copying", + format!( + "package dependency {}", + package.name.to_string().log_color_highlight() + ), + ); + + let _indent = LogIndent::new(); + for source in &package_sources.files { + let relative = source.strip_prefix(&def.config.source_wit_root)?; + let dest = target_wit_root.join(relative); + log_action( + "Copying", + format!( + "{} to {}", + source.log_color_highlight(), + dest.log_color_highlight() + ), + ); + fs::copy(source, &dest)?; + } + } + + write_embedded_source( + &target_deps.join("wasm-rpc"), + "wasm-rpc.wit", + golem_wasm_rpc::WASM_RPC_WIT, + )?; + + write_embedded_source( + &target_deps.join("io"), + "poll.wit", + golem_wasm_rpc::WASI_POLL_WIT, + )?; + + Ok(()) +} + +fn write_embedded_source(target_dir: &Path, file_name: &str, content: &str) -> anyhow::Result<()> { + fs::create_dir_all(target_dir)?; + + log_action( + "Writing", + format!( + "{} to {}", + file_name.log_color_highlight(), + target_dir.log_color_highlight() + ), + ); + + fs::write(target_dir.join(file_name), content)?; + + Ok(()) +} + +#[derive(PartialEq, Eq)] +pub enum UpdateCargoToml { + Update, + UpdateIfExists, + NoUpdate, +} + +pub struct AddStubAsDepConfig { + pub stub_wit_root: PathBuf, + pub dest_wit_root: PathBuf, + pub update_cargo_toml: UpdateCargoToml, +} + +pub fn add_stub_as_dependency_to_wit_dir(config: AddStubAsDepConfig) -> anyhow::Result<()> { + log_action( + "Adding", + format!( + "stub dependencies to {} from {}", + config.dest_wit_root.log_color_highlight(), + config.stub_wit_root.log_color_highlight() + ), + ); + + let _indent = LogIndent::new(); + + let stub_resolved_wit_root = ResolvedWitDir::new(&config.stub_wit_root)?; + let stub_package = stub_resolved_wit_root.main_package()?; + + let dest_resolved_wit_root = ResolvedWitDir::new(&config.dest_wit_root)?; + + let mut dest_encoded_wit_root = EncodedWitDir::new(&dest_resolved_wit_root.resolve)?; + + let mut actions = OverwriteSafeActions::new(); + let mut package_names_to_package_path = BTreeMap::::new(); + + for (package_name, package_id) in &stub_resolved_wit_root.resolve.package_names { + let package_sources = stub_resolved_wit_root + .package_sources + .get(package_id) + .ok_or_else(|| anyhow!("Failed to get package sources for {}", package_name))?; + + if *package_id == stub_resolved_wit_root.package_id { + let package_path = naming::wit::package_wit_dep_dir_from_parser(package_name); + + package_names_to_package_path.insert(package_name.clone(), package_path.clone()); + + for source in &package_sources.files { + actions.add(OverwriteSafeAction::CopyFile { + source: source.clone(), + target: config + .dest_wit_root + .join(naming::wit::DEPS_DIR) + .join(naming::wit::package_dep_dir_name_from_parser(package_name)) + .join(PathExtra::new(&source).file_name_to_string()?), + }); + } + } else { + package_names_to_package_path.insert( + package_name.clone(), + naming::wit::package_wit_dep_dir_from_package_dir_name( + &PathExtra::new(&package_sources.dir).file_name_to_string()?, + ), + ); + + for source in &package_sources.files { + actions.add(OverwriteSafeAction::CopyFile { + source: source.clone(), + target: config + .dest_wit_root + .join(PathExtra::new(&source).strip_prefix(&config.stub_wit_root)?), + }); + } + } + } + + // Import stub and remove source interfaces + let dest_main_package_id = dest_resolved_wit_root.package_id; + + let dest_main_package_sources = dest_resolved_wit_root + .package_sources + .get(&dest_main_package_id) + .ok_or_else(|| anyhow!("Failed to get dest main package sources"))?; + + if dest_main_package_sources.files.len() != 1 { + bail!( + "Expected exactly one dest main package source, got sources: {}", + dest_main_package_sources + .files + .iter() + .map(|s| s.log_color_highlight()) + .join(", ") + ); + } + + let package = dest_encoded_wit_root.package(dest_main_package_id)?; + // NOTE: wit_encoder "inlines" all transitive imports, so we have to clean up transitive + // imports from the source-interface package, given they might have been removed or renamed + // in the source, and could create invalid imports. + remove_world_named_interface_imports( + package, + &naming::wit::stub_import_interface_prefix_from_stub_package_name(&stub_package.name)?, + ); + add_world_named_interface_import(package, &naming::wit::stub_import_name(stub_package)?); + let content = package.to_string(); + + actions.add(OverwriteSafeAction::WriteFile { + content, + target: dest_main_package_sources.files[0].clone(), + }); + + // Check overwrites + // TODO: allow_skip_by_content, decide + let forbidden_overwrites = actions.run(true, false, log_action_plan)?; + if !forbidden_overwrites.is_empty() { + eprintln!("The following files would have been overwritten with new content:"); + for action in forbidden_overwrites { + eprintln!(" {}", action.target().display()); + } + eprintln!(); + eprintln!("Use --overwrite to force overwrite."); + } + + // Optionally update Cargo.toml + if let Some(target_parent) = config.dest_wit_root.parent() { + let target_cargo_toml = target_parent.join("Cargo.toml"); + if target_cargo_toml.exists() && target_cargo_toml.is_file() { + if config.update_cargo_toml != UpdateCargoToml::NoUpdate { + cargo::is_cargo_component_toml(&target_cargo_toml).context(format!( + "The file {target_cargo_toml:?} is not a valid cargo-component project" + ))?; + cargo::add_cargo_package_component_deps( + &target_cargo_toml, + package_names_to_package_path, + )?; + } + } else if config.update_cargo_toml == UpdateCargoToml::Update { + return Err(anyhow!( + "Cannot update {:?} file because it does not exist or is not a file", + target_cargo_toml.log_color_highlight() + )); + } + } else if config.update_cargo_toml == UpdateCargoToml::Update { + return Err(anyhow!("Cannot update the Cargo.toml file because parent directory of the destination WIT root does not exist.")); + } + + Ok(()) +} + +trait ToEncoder { + type EncoderType; + fn to_encoder(&self, stub_definition: &StubDefinition) -> anyhow::Result; +} + +impl ToEncoder for wit_parser::Type { + type EncoderType = Type; + + fn to_encoder(&self, def: &StubDefinition) -> anyhow::Result { + Ok(match self { + wit_parser::Type::Bool => Type::Bool, + wit_parser::Type::U8 => Type::U8, + wit_parser::Type::U16 => Type::U16, + wit_parser::Type::U32 => Type::U32, + wit_parser::Type::U64 => Type::U64, + wit_parser::Type::S8 => Type::S8, + wit_parser::Type::S16 => Type::S16, + wit_parser::Type::S32 => Type::S32, + wit_parser::Type::S64 => Type::S64, + wit_parser::Type::F32 => Type::F32, + wit_parser::Type::F64 => Type::F64, + wit_parser::Type::Char => Type::Char, + wit_parser::Type::String => Type::String, + wit_parser::Type::Id(type_id) => { + if let Some(type_alias) = def.get_stub_used_type_alias(*type_id) { + Type::Named(Ident::new(type_alias.to_string())) + } else { + let typ = def.get_type_def(*type_id)?; + + let context = || { + anyhow!( + "Failed to convert wit parser type to encoder type, type: {:?}", + typ + ) + }; + + match &typ.kind { + wit_parser::TypeDefKind::Option(inner) => Type::option( + inner + .to_encoder(def) + .context("Failed to convert option inner type") + .with_context(context)?, + ), + wit_parser::TypeDefKind::List(inner) => Type::list( + inner + .to_encoder(def) + .context("Failed to convert list inner type") + .with_context(context)?, + ), + wit_parser::TypeDefKind::Tuple(tuple) => Type::tuple( + tuple + .types + .iter() + .map(|t| { + t.to_encoder(def).with_context(|| { + anyhow!("Failed to convert tuple elem type: {:?}", t) + }) + }) + .collect::, _>>() + .with_context(context)?, + ), + wit_parser::TypeDefKind::Result(result) => { + match (&result.ok, &result.err) { + (Some(ok), Some(err)) => Type::result_both( + ok.to_encoder(def) + .context("Failed to convert result ok type") + .with_context(context)?, + err.to_encoder(def) + .context("Failed to convert result error type") + .with_context(context)?, + ), + (Some(ok), None) => Type::result_ok( + ok.to_encoder(def) + .context("Failed to convert result ok type (no error case)") + .with_context(context)?, + ), + (None, Some(err)) => Type::result_err( + err.to_encoder(def) + .context("Failed to convert result error type (no ok case)") + .with_context(context)?, + ), + (None, None) => Err(anyhow!("Result type has no ok or err types")) + .with_context(context)?, + } + } + wit_parser::TypeDefKind::Handle(handle) => match handle { + wit_parser::Handle::Own(type_id) => { + wit_parser::Type::Id(*type_id).to_encoder(def)? + } + wit_parser::Handle::Borrow(type_id) => Type::borrow( + wit_parser::Type::Id(*type_id).to_encoder(def)?.to_string(), + ), + }, + _ => { + let name = typ + .name + .clone() + .ok_or_else(|| anyhow!("Expected name for type: {:?}", typ)) + .with_context(context)?; + Type::Named(Ident::new(name)) + } + } + } + } + }) + } +} + +impl ToEncoder for Vec { + type EncoderType = Params; + + fn to_encoder(&self, def: &StubDefinition) -> anyhow::Result { + Ok(Params::from_iter( + self.iter() + .map(|param| { + param + .typ + .to_encoder(def) + .with_context(|| { + anyhow!( + "Failed to convert parameter to encoder type, parameter: {:?}", + param + ) + }) + .map(|typ| (param.name.clone(), typ)) + }) + .collect::, _>>() + .with_context(|| anyhow!("Failed to convert params to encoder type: {:?}", self))?, + )) + } +} + +impl ToEncoder for FunctionResultStub { + type EncoderType = Results; + + fn to_encoder(&self, def: &StubDefinition) -> anyhow::Result { + let context = || { + anyhow!( + "Failed to convert function result stub to encoder type: {:?}", + self + ) + }; + Ok(match self { + FunctionResultStub::Anon(typ) => { + Results::Anon(typ.to_encoder(def).with_context(context)?) + } + FunctionResultStub::Named(types) => { + Results::Named(types.to_encoder(def).with_context(context)?) + } + FunctionResultStub::SelfType => { + Err(anyhow!("Unexpected self type")).with_context(context)? + } + }) + } +} + +fn add_world_named_interface_import(package: &mut Package, import_name: &str) { + for world_item in package.items_mut() { + if let PackageItem::World(world) = world_item { + let is_already_imported = world.items_mut().iter().any(|item| { + if let WorldItem::NamedInterfaceImport(import) = item { + import.name().raw_name() == import_name + } else { + false + } + }); + if !is_already_imported { + world.named_interface_import(import_name.to_string()); + } + } + } +} + +fn remove_world_named_interface_imports(package: &mut Package, import_prefix: &str) { + for world_item in package.items_mut() { + if let PackageItem::World(world) = world_item { + world.items_mut().retain(|item| { + if let WorldItem::NamedInterfaceImport(import) = item { + !import.name().raw_name().starts_with(import_prefix) + } else { + true + } + }) + } + } +} + +pub fn extract_main_interface_as_wit_dep(wit_dir: &Path) -> anyhow::Result<()> { + log_action( + "Extracting", + format!( + "interface package from main package in wit directory {}", + wit_dir.log_color_highlight() + ), + ); + + let resolved_wit_dir = ResolvedWitDir::new(wit_dir)?; + let main_package_id = resolved_wit_dir.package_id; + let mut encoded_wit_dir = EncodedWitDir::new(&resolved_wit_dir.resolve)?; + + let resolved_wit_dir = ResolvedWitDir::new(wit_dir)?; + + let (main_package, interface_package) = + extract_main_interface_package(main_package_id, &mut encoded_wit_dir)?; + let sources = resolved_wit_dir + .package_sources + .get(&resolved_wit_dir.package_id) + .ok_or_else(|| { + anyhow!( + "Failed to get sources for main package, wit dir: {}", + wit_dir.log_color_highlight() + ) + })?; + + if sources.files.len() != 1 { + bail!( + "Expected exactly one source for main package, wit dir: {}", + wit_dir.log_color_highlight() + ); + } + + let _indent = LogIndent::new(); + + let interface_package_path = wit_dir + .join(naming::wit::DEPS_DIR) + .join(package_dep_dir_name_from_encoder(interface_package.name())) + .join(naming::wit::INTERFACE_WIT_FILE_NAME); + log_action( + "Writing", + format!( + "interface package to {}", + interface_package_path.log_color_highlight() + ), + ); + fs::write_str(&interface_package_path, interface_package.to_string())?; + + let main_package_path = &sources.files[0]; + log_action( + "Writing", + format!( + "main package to {}", + main_package_path.log_color_highlight() + ), + ); + fs::write_str(main_package_path, main_package.to_string())?; + + Ok(()) +} + +// TODO: handle world include +// TODO: handle world use +// TODO: maybe transform inline interfaces and functions into included world? +fn extract_main_interface_package( + main_package_id: PackageId, + encoded_wit_dir: &mut EncodedWitDir, +) -> anyhow::Result<(Package, Package)> { + let package = encoded_wit_dir.package(main_package_id)?; + + let mut interface_package = package.clone(); + interface_package.set_name(naming::wit::interface_encoder_package_name(package.name())); + + let interface_prefix = format!( + "{}:{}/", + package.name().namespace(), + interface_package.name().name() + ); + let interface_suffix = package + .name() + .version() + .map(|version| format!("@{}", version)) + .unwrap_or_default(); + + let mut exported_interface_identifiers = HashSet::::new(); + let mut inline_interface_exports = BTreeMap::>::new(); + let mut inline_function_exports = BTreeMap::>::new(); + for package_item in package.items_mut() { + if let PackageItem::World(world) = package_item { + let world_name = world.name().clone(); + + world.items_mut().retain(|world_item| match world_item { + // Remove and collect inline interface exports + WorldItem::InlineInterfaceExport(interface) => { + let mut interface = interface.clone(); + interface.set_name(naming::wit::interface_package_world_inline_interface_name( + &world_name, + interface.name(), + )); + + inline_interface_exports + .entry(world_name.clone()) + .or_default() + .push(interface.clone()); + false + } + // Remove and collect inline function exports + WorldItem::FunctionExport(function) => { + inline_function_exports + .entry(world_name.clone()) + .or_default() + .push(function.clone()); + false + } + // Collect named interface export identifiers + WorldItem::NamedInterfaceExport(interface) => { + exported_interface_identifiers.insert(interface.name().clone()); + true + } + _ => true, + }); + + // Insert named imports for extracted inline interfaces + if let Some(interfaces) = inline_interface_exports.get(&world_name) { + for interface in interfaces { + world.named_interface_export(interface.name().clone()); + } + } + + // Insert named import for extracted inline functions + if inline_function_exports.contains_key(&world_name) { + world.named_interface_export(format!( + "{}{}{}", + interface_prefix, + naming::wit::interface_package_world_inline_functions_interface_name( + &world_name + ), + interface_suffix + )); + } + } + } + + package.items_mut().retain(|item| match item { + // Drop exported interfaces from original package + PackageItem::Interface(interface) => { + !exported_interface_identifiers.contains(interface.name()) + } + PackageItem::World(_) => true, + }); + + interface_package.items_mut().retain(|item| match item { + // Drop non-exported interfaces from interface package + PackageItem::Interface(interface) => { + exported_interface_identifiers.contains(interface.name()) + } + // Drop all worlds from interface package + PackageItem::World(_) => false, + }); + + // Rename named self export and imports to use the extracted interface names + for package_item in package.items_mut() { + if let PackageItem::World(world) = package_item { + for world_item in world.items_mut() { + if let WorldItem::NamedInterfaceImport(import) = world_item { + if !import.name().raw_name().contains("/") { + import.set_name(format!( + "{}{}{}", + interface_prefix, + import.name(), + interface_suffix + )); + } + } else if let WorldItem::NamedInterfaceExport(export) = world_item { + if !export.name().raw_name().contains("/") { + export.set_name(format!( + "{}{}{}", + interface_prefix, + export.name(), + interface_suffix + )); + } + } + } + } + } + + // Add inlined exported interfaces to the interface package + for (_, interfaces) in inline_interface_exports { + for interface in interfaces { + interface_package.interface(interface); + } + } + + // Add interface for inlined functions to the interface package + for (world_name, functions) in inline_function_exports { + let mut interface = Interface::new( + naming::wit::interface_package_world_inline_functions_interface_name(&world_name), + ); + + for function in functions { + interface.function(function); + } + + interface_package.interface(interface); + } + + Ok((package.clone(), interface_package)) +} diff --git a/wasm-rpc-stubgen/src/wit_resolve.rs b/wasm-rpc-stubgen/src/wit_resolve.rs index 442a0187..56a1fdfb 100644 --- a/wasm-rpc-stubgen/src/wit_resolve.rs +++ b/wasm-rpc-stubgen/src/wit_resolve.rs @@ -1,14 +1,28 @@ -use crate::fs::strip_path_prefix; -use anyhow::{anyhow, bail, Context}; +use crate::fs::PathExtra; +use crate::log::{log_action, LogColorize, LogIndent}; +use crate::model::app::{Application, ComponentName, ComponentPropertiesExtensions, ProfileName}; +use crate::validation::{ValidatedResult, ValidationBuilder}; +use crate::{fs, naming}; +use anyhow::{anyhow, bail, Context, Error}; use indexmap::IndexMap; +use indoc::formatdoc; +use itertools::Itertools; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::path::{Path, PathBuf}; -use wit_parser::{Package, PackageId, Resolve, UnresolvedPackageGroup}; +use wit_parser::{ + Package, PackageId, PackageName, PackageSourceMap, Resolve, UnresolvedPackageGroup, +}; + +pub struct PackageSource { + pub dir: PathBuf, + pub files: Vec, +} pub struct ResolvedWitDir { pub path: PathBuf, pub resolve: Resolve, pub package_id: PackageId, - pub sources: IndexMap)>, + pub package_sources: IndexMap, } impl ResolvedWitDir { @@ -21,7 +35,17 @@ impl ResolvedWitDir { anyhow!( "Failed to get package by id: {:?}, wit dir: {}", package_id, - self.path.to_string_lossy() + self.path.log_color_highlight() + ) + }) + } + + pub fn package_sources(&self, package_id: PackageId) -> Result<&PackageSource, Error> { + self.package_sources.get(&package_id).with_context(|| { + anyhow!( + "Failed to get package sources by id: {:?}, wit dir: {}", + package_id, + self.path.log_color_highlight() ) }) } @@ -37,40 +61,27 @@ fn resolve_wit_dir(path: &Path) -> anyhow::Result { let mut resolve = Resolve::new(); - let (package_id, sources) = resolve + let (package_id, package_source_map) = resolve .push_dir(path) - .with_context(|| anyhow!("Failed to resolve wit dir: {}", path.to_string_lossy()))?; + .with_context(|| anyhow!("Failed to resolve wit dir: {}", path.log_color_highlight()))?; - let sources = partition_sources_by_package_ids(path, &resolve, package_id, sources)?; + let package_sources = collect_package_sources(path, &resolve, package_id, &package_source_map)?; Ok(ResolvedWitDir { path: path.to_path_buf(), resolve, package_id, - sources, + package_sources, }) } -// Currently Resolve::push_dir does return the source that were used during resolution, -// but they are not partitioned by packages, and the source info is not kept inside Resolve -// (it is lost during resolution). -// -// To solve this (until we can get a better API upstream) we could extract and replicate the logic used -// there, but that would require many code duplication, as many functions and types are not public. -// -// Instead of that, we partition the returned sources by following the accepted file and directory structure. -// -// Unfortunately we still have some duplication of performed operations: during partitioning we partially parse -// the dependencies again - as UnresolvedPackageGroups - but similar partial "peeks" into deps already happened -// in stubgen steps before. This way we try to pull and concentrate them here, so they only happen when creating -// a new ResolvedWitDir, and parsing should only happen twice: while using Resolve::push_dir above, and here. -fn partition_sources_by_package_ids( +fn collect_package_sources( path: &Path, resolve: &Resolve, root_package_id: PackageId, - sources: Vec, -) -> anyhow::Result)>> { - // Based on Resolve::push_dir (): + package_source_map: &PackageSourceMap, +) -> anyhow::Result> { + // Based on Resolve::push_dir: // // The deps folder may contain: // $path/ deps/ my-package/*.wit: a directory that may contain multiple WIT files @@ -80,67 +91,711 @@ fn partition_sources_by_package_ids( // Disabling "wasm" and "wat" sources could be done by disabling default features, but they are also required through other dependencies, // so we filter out currently not supported path patterns here, including the single file format (for now). - let mut partitioned_sources = IndexMap::)>::new(); - let mut dep_package_path_to_package_id = IndexMap::::new(); + let deps_dir = path.join("deps"); + let mut package_dir_paths = IndexMap::::new(); + for (package_id, package) in &resolve.packages { + let sources = package_source_map + .package_paths(package_id) + .ok_or_else(|| { + anyhow!( + "Failed to get package source map for package {}", + package.name.to_string().log_color_highlight() + ) + })? + .map(|path| path.to_path_buf()) + .collect::>(); - for source in sources { - let relative_source = strip_path_prefix(path, &source)?; + if package_id == root_package_id { + package_dir_paths.insert( + package_id, + PackageSource { + dir: path.to_path_buf(), + files: sources, + }, + ); + } else { + if sources.is_empty() { + bail!( + "Expected at least one source for package: {}", + package.name.to_string().log_color_error_highlight() + ); + }; + + let source = &sources[0]; - let segments = relative_source.iter().collect::>(); + let extension = source.extension().ok_or_else(|| { + anyhow!( + "Failed to get extension for wit source: {}", + source.log_color_highlight() + ) + })?; - let (package_path, package_id) = match segments.len() { - 1 => (path, root_package_id), - 2 => { + if extension != "wit" { bail!( - "Single file with packages not supported, source: {}", - source.to_string_lossy() + "Only wit sources are supported, source: {}", + source.log_color_highlight() ); } - 3 => { - let dep_package_path = source.parent().ok_or_else(|| { - anyhow!( - "Failed to get source parent, source: {}", - source.to_string_lossy() - ) - })?; - - match dep_package_path_to_package_id.get(dep_package_path) { - Some(package_id) => (dep_package_path, *package_id), - None => { - let package_id = *resolve - .package_names - .get( - &UnresolvedPackageGroup::parse_dir(dep_package_path)? - .main - .name, + + let parent = source.parent().ok_or_else(|| { + anyhow!( + "Failed to get parent for wit source: {}", + source.log_color_highlight() + ) + })?; + + if parent == deps_dir { + bail!( + "Single-file wit packages without folder are not supported, source: {}", + source.log_color_highlight() + ); + } + + package_dir_paths.insert( + package_id, + PackageSource { + dir: parent.to_path_buf(), + files: sources, + }, + ); + } + } + Ok(package_dir_paths) +} + +pub struct ResolvedWitComponent { + main_package_name: PackageName, + resolved_generated_wit_dir: Option, + app_component_deps: HashSet, + source_referenced_package_deps: HashSet, + source_contained_package_deps: HashSet, + source_component_deps: BTreeSet, // NOTE: BTree for making dep sorting deterministic + generated_component_deps: Option>, +} + +pub struct ResolvedWitApplication { + components: BTreeMap, // NOTE: BTree for making dep sorting deterministic + package_to_component: HashMap, + stub_package_to_component: HashMap, + interface_package_to_component: HashMap, + component_order: Vec, +} + +impl ResolvedWitApplication { + pub fn new( + app: &Application, + profile: Option<&ProfileName>, + ) -> ValidatedResult { + log_action("Resolving", "application wit directories"); + let _indent = LogIndent::new(); + + let mut resolved_app = Self { + components: Default::default(), + package_to_component: Default::default(), + stub_package_to_component: Default::default(), + interface_package_to_component: Default::default(), + component_order: Default::default(), + }; + + let mut validation = ValidationBuilder::new(); + + resolved_app.add_components_from_app(&mut validation, app, profile); + + resolved_app.validate_package_names(&mut validation); + resolved_app.collect_component_deps(app, &mut validation); + resolved_app.sort_components_by_source_deps(&mut validation); + + validation.build(resolved_app) + } + + fn validate_package_names(&self, validation: &mut ValidationBuilder) { + if self.package_to_component.len() != self.components.len() { + let mut package_names_to_component_names = + BTreeMap::<&PackageName, Vec<&ComponentName>>::new(); + for (component_name, component) in &self.components { + package_names_to_component_names + .entry(&component.main_package_name) + .and_modify(|component_names| component_names.push(component_name)) + .or_insert_with(|| vec![&component_name]); + } + + validation.add_errors( + package_names_to_component_names, + |(package_name, component_names)| { + (component_names.len() > 1).then(|| { + ( + vec![ + ("package name", package_name.to_string()), + ( + "component names", + component_names.iter().map(|s| s.as_str()).join(", "), + ), + ], + "Same package name is used for multiple components".to_string(), + ) + }) + }, + ); + } + } + + fn add_resolved_component( + &mut self, + component_name: ComponentName, + resolved_component: ResolvedWitComponent, + ) { + self.package_to_component.insert( + resolved_component.main_package_name.clone(), + component_name.clone(), + ); + self.stub_package_to_component.insert( + naming::wit::stub_package_name(&resolved_component.main_package_name), + component_name.clone(), + ); + self.interface_package_to_component.insert( + naming::wit::interface_parser_package_name(&resolved_component.main_package_name), + component_name.clone(), + ); + self.components.insert(component_name, resolved_component); + } + + fn add_components_from_app( + &mut self, + validation: &mut ValidationBuilder, + app: &Application, + profile: Option<&ProfileName>, + ) { + for component_name in app.component_names() { + validation.push_context("component name", component_name.to_string()); + + let source_wit_dir = app.component_source_wit(component_name, profile); + let generated_wit_dir = app.component_generated_wit(component_name, profile); + + log_action( + "Resolving", + format!( + "component wit dirs for {} ({}, {})", + component_name.as_str().log_color_highlight(), + source_wit_dir.log_color_highlight(), + generated_wit_dir.log_color_highlight(), + ), + ); + + let resolved_component = (|| -> anyhow::Result { + let unresolved_source_package_group = + UnresolvedPackageGroup::parse_dir(&source_wit_dir).with_context(|| { + anyhow!( + "Failed to parse component {} main package in source wit dir {}", + component_name.as_str().log_color_error_highlight(), + source_wit_dir.log_color_highlight(), + ) + })?; + + let source_referenced_package_deps = unresolved_source_package_group + .main + .foreign_deps + .keys() + .cloned() + .collect(); + + let source_contained_package_deps = { + let deps_path = + Path::new(&app.component_properties(component_name, profile).source_wit) + .join("deps"); + if !deps_path.exists() { + HashSet::new() + } else { + parse_wit_deps_dir(&deps_path)? + .into_iter() + .map(|package_group| package_group.main.name) + .collect::>() + } + }; + + let main_package_name = unresolved_source_package_group.main.name.clone(); + + let resolved_generated_wit_dir = ResolvedWitDir::new(&generated_wit_dir).ok(); + let generated_has_same_main_package_name = resolved_generated_wit_dir + .as_ref() + .map(|wit| wit.main_package()) + .transpose()? + .map(|generated_main_package| main_package_name == generated_main_package.name) + .unwrap_or_default(); + let resolved_generated_wit_dir = generated_has_same_main_package_name + .then_some(resolved_generated_wit_dir) + .flatten(); + + let app_component_deps = app + .component_wasm_rpc_dependencies(component_name) + .iter() + .cloned() + .collect(); + + Ok(ResolvedWitComponent { + main_package_name, + resolved_generated_wit_dir, + app_component_deps, + source_referenced_package_deps, + source_contained_package_deps, + source_component_deps: Default::default(), + generated_component_deps: Default::default(), + }) + })(); + + match resolved_component { + Ok(resolved_component) => { + self.add_resolved_component(component_name.clone(), resolved_component); + } + Err(err) => validation.add_error(format!("{:?}", err)), + } + + validation.pop_context(); + } + } + + fn collect_component_deps( + &mut self, + app: &Application, + validation: &mut ValidationBuilder, + ) { + fn component_deps< + 'a, + I: IntoIterator, + O: FromIterator, + >( + known_package_deps: &HashMap, + dep_package_names: I, + ) -> O { + dep_package_names + .into_iter() + .filter_map(|package_name| known_package_deps.get(package_name).cloned()) + .collect() + } + + let mut deps = HashMap::< + ComponentName, + (BTreeSet, Option>), + >::new(); + for (component_name, component) in &self.components { + deps.insert( + component_name.clone(), + ( + component_deps( + &self.interface_package_to_component, + &component.source_referenced_package_deps, + ), + component + .resolved_generated_wit_dir + .as_ref() + .map(|wit_dir| { + component_deps( + &self.stub_package_to_component, + wit_dir.resolve.package_names.keys(), ) - .ok_or_else(|| { - anyhow!( - "Failed to get package id for source: {}", - source.to_string_lossy(), - ) - })?; + }), + ), + ); + } + for (component_name, (source_deps, generated_deps)) in deps { + let component = self.components.get_mut(&component_name).unwrap(); + component.source_component_deps = source_deps; + component.generated_component_deps = generated_deps; + } + + for (component_name, component) in &self.components { + let main_deps: HashSet = component_deps( + &self.package_to_component, + &component.source_referenced_package_deps, + ); + + let stub_deps: HashSet = component_deps( + &self.stub_package_to_component, + &component.source_referenced_package_deps, + ); + + if !main_deps.is_empty() || !stub_deps.is_empty() { + validation.push_context("component name", component_name.to_string()); + validation.push_context("package name", component.main_package_name.to_string()); + { + validation.push_context( + "source", + app.component_source_dir(component_name) + .to_string_lossy() + .to_string(), + ); + } + + for dep_component_name in main_deps { + let dep_package_name = &self + .component(&dep_component_name) + .unwrap() + .main_package_name; + + validation + .push_context("referenced package name", dep_package_name.to_string()); - dep_package_path_to_package_id - .insert(dep_package_path.to_path_buf(), package_id); + validation.add_error(formatdoc!(" + Direct WIT package reference to component {} main package {} is not supported. + For using component stubs, declare them in the app manifest. + For using exported types from another component, use the component interface package (e.g.: ns:package-name-interface).", + dep_component_name.to_string().log_color_highlight(), + dep_package_name.to_string().log_color_error_highlight() + )); - (dep_package_path, package_id) + validation.pop_context(); + } + + for dep_component_name in stub_deps { + let dep_package_name = &self + .component(&dep_component_name) + .unwrap() + .main_package_name; + + validation + .push_context("referenced package name", dep_package_name.to_string()); + + validation.add_error(formatdoc!(" + Direct WIT package reference to component {} stub packages {} is not supported. + For using component stubs, declare them in the app manifest. + For using exported types from another component, use the component interface package (e.g.: ns:package-name-interface).", + dep_component_name.to_string().log_color_highlight(), + dep_package_name.to_string().log_color_error_highlight() + )); + + validation.pop_context(); + } + + validation.pop_context(); + validation.pop_context(); + } + } + } + + fn sort_components_by_source_deps(&mut self, validation: &mut ValidationBuilder) { + struct Visit<'a> { + resolved_app: &'a ResolvedWitApplication, + component_names_by_id: Vec<&'a ComponentName>, + component_names_to_id: HashMap<&'a ComponentName, usize>, + visited: HashSet, + visiting: HashSet, + path: Vec, + component_order: Vec, + } + + impl<'a> Visit<'a> { + fn new(resolved_app: &'a ResolvedWitApplication) -> Self { + let component_names_by_id = resolved_app.components.keys().collect::>(); + let component_names_to_id = component_names_by_id + .iter() + .enumerate() + .map(|(id, name)| (*name, id)) + .collect(); + + Self { + resolved_app, + component_names_by_id, + component_names_to_id, + visited: Default::default(), + visiting: Default::default(), + path: vec![], + component_order: vec![], + } + } + + fn visit_all(mut self) -> Result, Vec<&'a ComponentName>> { + for (component_id, &component_name) in + self.component_names_by_id.clone().iter().enumerate() + { + if !self.visited.contains(&component_id) + && !self.visit(component_id, component_name) + { + return Err(self + .path + .into_iter() + .map(|component_id| self.component_names_by_id[component_id]) + .collect::>()); } } + Ok(self.component_order) } - _ => { - bail!( - "Unexpected source path, source: {}", - source.to_string_lossy() - ); + + fn visit(&mut self, component_id: usize, component_name: &ComponentName) -> bool { + if self.visited.contains(&component_id) { + true + } else if self.visiting.contains(&component_id) { + self.path.push(component_id); + false + } else { + let component = self.resolved_app.components.get(component_name).unwrap(); + + self.path.push(component_id); + self.visiting.insert(component_id); + + for dep_component_name in &component.source_component_deps { + if !self.visit( + self.component_names_to_id[dep_component_name], + dep_component_name, + ) { + return false; + } + } + + self.visiting.remove(&component_id); + self.visited.insert(component_id); + self.component_order.push(component_name.clone()); + self.path.pop(); + + true + } } - }; + } + + match Visit::new(self).visit_all() { + Ok(component_order) => { + self.component_order = component_order; + } + Err(recursive_path) => { + validation.add_error(format!( + "Found component interface package use cycle: {}", + recursive_path + .iter() + .map(|s| s.as_str().log_color_error_highlight()) + .join(", ") + )); + } + } + } + + pub fn component_order(&self) -> &[ComponentName] { + &self.component_order + } + + pub fn component_order_cloned(&self) -> Vec { + self.component_order.clone() + } + + fn component(&self, component_name: &ComponentName) -> Result<&ResolvedWitComponent, Error> { + self.components.get(component_name).ok_or_else(|| { + anyhow!( + "Component not found: {}", + component_name.as_str().log_color_error_highlight() + ) + }) + } + + // NOTE: Intended to be used for non-component wit package deps, so it does not include + // component interface packages, as those are added from stubs + pub fn missing_generic_source_package_deps( + &self, + component_name: &ComponentName, + ) -> anyhow::Result> { + let component = self.component(component_name)?; + Ok(component + .source_referenced_package_deps + .iter() + .filter(|&package_name| { + !component + .source_contained_package_deps + .contains(package_name) + && !self + .interface_package_to_component + .contains_key(package_name) + }) + .cloned() + .collect::>()) + } + + pub fn component_interface_package_deps( + &self, + component_name: &ComponentName, + ) -> anyhow::Result> { + let component = self.component(component_name)?; + Ok(component + .source_referenced_package_deps + .iter() + .filter_map(|package_name| { + match self.interface_package_to_component.get(package_name) { + Some(dep_component_name) if dep_component_name != component_name => { + Some((package_name.clone(), dep_component_name.clone())) + } + _ => None, + } + }) + .collect()) + } + + // NOTE: this does not mean that the dependencies themselves are up-to-date, rather + // only checks if there are difference in set of dependencies specified in the + // application model vs in wit dependencies + pub fn is_dep_graph_up_to_date(&self, component_name: &ComponentName) -> anyhow::Result { + let component = self.component(component_name)?; + Ok(match &component.generated_component_deps { + Some(generated_deps) => &component.app_component_deps == generated_deps, + None => false, + }) + } + + // NOTE: this does not mean that the dependency itself is up-to-date, rather + // only checks if it is present as wit package dependency + pub fn has_as_wit_dep( + &self, + component_name: &ComponentName, + dep_component_name: &ComponentName, + ) -> anyhow::Result { + let component = self.component(component_name)?; - partitioned_sources - .entry(package_id) - .and_modify(|(_, sources)| sources.push(source.to_path_buf())) - .or_insert_with(|| (package_path.to_path_buf(), vec![source.to_path_buf()])); + Ok(match &component.generated_component_deps { + Some(generated_deps) => generated_deps.contains(dep_component_name), + None => false, + }) } +} - Ok(partitioned_sources) +pub fn parse_wit_deps_dir(path: &Path) -> Result, Error> { + let mut entries = path + .read_dir() + .and_then(|read_dir| read_dir.collect::>>()) + .with_context(|| { + anyhow!( + "Failed to read wit dependencies from {}", + path.log_color_error_highlight() + ) + })?; + entries.sort_by_key(|e| e.file_name()); + entries + .iter() + .filter_map(|entry| { + let path = entry.path(); + // NOTE: unlike wit_resolve - for now - we do not support: + // - symlinks + // - single file deps + // - wasm or wat deps + path.is_dir().then(|| { + UnresolvedPackageGroup::parse_dir(&path).with_context(|| { + anyhow!( + "Failed to parse wit dependency package {}", + path.log_color_error_highlight() + ) + }) + }) + }) + .collect::, _>>() +} + +pub struct WitDepsResolver { + sources: Vec, + packages: HashMap>, +} + +impl WitDepsResolver { + pub fn new(sources: Vec) -> anyhow::Result { + let mut packages = HashMap::>::new(); + + for source in &sources { + packages.insert( + source.clone(), + parse_wit_deps_dir(source)? + .into_iter() + .map(|package| (package.main.name.clone(), package)) + .collect(), + ); + } + + Ok(Self { sources, packages }) + } + + pub fn package(&self, package_name: &PackageName) -> anyhow::Result<&UnresolvedPackageGroup> { + for source in &self.sources { + if let Some(package) = self.packages.get(source).unwrap().get(package_name) { + return Ok(package); + } + } + bail!( + "Package {} not found, sources searched: {}", + package_name.to_string().log_color_error_highlight(), + if self.sources.is_empty() { + "no sources were provided".to_string() + } else { + self.sources + .iter() + .map(|s| s.log_color_highlight()) + .join(", ") + } + ) + } + + pub fn package_sources( + &self, + package_name: &PackageName, + ) -> anyhow::Result> { + self.package(package_name) + .map(|package| package.source_map.source_files()) + } + + pub fn package_names_with_transitive_deps( + &self, + packages: &[PackageName], + ) -> anyhow::Result> { + fn visit( + resolver: &WitDepsResolver, + all_package_names: &mut HashSet, + package_name: &PackageName, + ) -> anyhow::Result<()> { + if !all_package_names.contains(package_name) { + let package = resolver.package(package_name)?; + all_package_names.insert(package_name.clone()); + for package_name in package.main.foreign_deps.keys() { + visit(resolver, all_package_names, package_name)?; + } + } + + Ok(()) + } + + let mut all_package_names = HashSet::::new(); + for package_name in packages { + visit(self, &mut all_package_names, package_name)?; + } + Ok(all_package_names) + } + + pub fn add_packages_with_transitive_deps_to_wit_dir( + &self, + packages: &[PackageName], + target_deps_dir: &Path, + ) -> anyhow::Result<()> { + for package_name in self.package_names_with_transitive_deps(packages)? { + log_action( + "Adding", + format!( + "package dependency {} to {}", + package_name.to_string().log_color_highlight(), + target_deps_dir.log_color_highlight(), + ), + ); + let _indent = LogIndent::new(); + + for source in self.package_sources(&package_name)? { + let source = PathExtra::new(source); + let source_parent = PathExtra::new(source.parent()?); + + let target = target_deps_dir + .join(naming::wit::DEPS_DIR) + .join(source_parent.file_name_to_string()?) + .join(source.file_name_to_string()?); + + log_action( + "Copying", + format!( + "package dependency source from {} to {}", + source.log_color_highlight(), + target.log_color_highlight() + ), + ); + fs::copy(source, target)?; + } + } + + Ok(()) + } } diff --git a/wasm-rpc-stubgen/test-data/many-ways-to-export/deps/sub/sub.wit b/wasm-rpc-stubgen/test-data/many-ways-to-export/deps/sub/sub.wit deleted file mode 100644 index b62d424e..00000000 --- a/wasm-rpc-stubgen/test-data/many-ways-to-export/deps/sub/sub.wit +++ /dev/null @@ -1,5 +0,0 @@ -package test:sub; - -interface iface4 { - func5: func(); -} diff --git a/wasm-rpc-stubgen/test-data/many-ways-to-export/main.wit b/wasm-rpc-stubgen/test-data/many-ways-to-export/main.wit deleted file mode 100644 index 1ab0660e..00000000 --- a/wasm-rpc-stubgen/test-data/many-ways-to-export/main.wit +++ /dev/null @@ -1,32 +0,0 @@ -// Example of many ways to export functions, including top-level, multiple interfaces, use and import statements, etc - -package test:exports; - -interface iface1 { - func2: func(); -} - -interface iface2 { - use iface3.{color}; - func3: func() -> color; -} - -interface iface3 { - enum color { - red, - green, - blue - } -} - -world api { - export func1: func(); - export iface1; - export iface2; - - export inline-iface: interface { - func4: func(); - } - - export test:sub/iface4; -} \ No newline at end of file diff --git a/wasm-rpc-stubgen/test-data/all-wit-types-alternative/main.wit b/wasm-rpc-stubgen/test-data/wit/all-wit-types-alternative/main.wit similarity index 100% rename from wasm-rpc-stubgen/test-data/all-wit-types-alternative/main.wit rename to wasm-rpc-stubgen/test-data/wit/all-wit-types-alternative/main.wit diff --git a/wasm-rpc-stubgen/test-data/all-wit-types/main.wit b/wasm-rpc-stubgen/test-data/wit/all-wit-types/main.wit similarity index 100% rename from wasm-rpc-stubgen/test-data/all-wit-types/main.wit rename to wasm-rpc-stubgen/test-data/wit/all-wit-types/main.wit diff --git a/wasm-rpc-stubgen/test-data/caller-no-dep-importstub/Cargo.toml b/wasm-rpc-stubgen/test-data/wit/caller-no-dep-importstub/Cargo.toml similarity index 100% rename from wasm-rpc-stubgen/test-data/caller-no-dep-importstub/Cargo.toml rename to wasm-rpc-stubgen/test-data/wit/caller-no-dep-importstub/Cargo.toml diff --git a/wasm-rpc-stubgen/test-data/caller-no-dep-importstub/src/lib.rs b/wasm-rpc-stubgen/test-data/wit/caller-no-dep-importstub/src/lib.rs similarity index 100% rename from wasm-rpc-stubgen/test-data/caller-no-dep-importstub/src/lib.rs rename to wasm-rpc-stubgen/test-data/wit/caller-no-dep-importstub/src/lib.rs diff --git a/wasm-rpc-stubgen/test-data/caller-no-dep/wit/caller.wit b/wasm-rpc-stubgen/test-data/wit/caller-no-dep-importstub/wit/caller.wit similarity index 100% rename from wasm-rpc-stubgen/test-data/caller-no-dep/wit/caller.wit rename to wasm-rpc-stubgen/test-data/wit/caller-no-dep-importstub/wit/caller.wit diff --git a/wasm-rpc-stubgen/test-data/caller-no-dep/Cargo.toml b/wasm-rpc-stubgen/test-data/wit/caller-no-dep/Cargo.toml similarity index 100% rename from wasm-rpc-stubgen/test-data/caller-no-dep/Cargo.toml rename to wasm-rpc-stubgen/test-data/wit/caller-no-dep/Cargo.toml diff --git a/wasm-rpc-stubgen/test-data/caller-no-dep/src/lib.rs b/wasm-rpc-stubgen/test-data/wit/caller-no-dep/src/lib.rs similarity index 100% rename from wasm-rpc-stubgen/test-data/caller-no-dep/src/lib.rs rename to wasm-rpc-stubgen/test-data/wit/caller-no-dep/src/lib.rs diff --git a/wasm-rpc-stubgen/test-data/caller-no-dep-importstub/wit/caller.wit b/wasm-rpc-stubgen/test-data/wit/caller-no-dep/wit/caller.wit similarity index 68% rename from wasm-rpc-stubgen/test-data/caller-no-dep-importstub/wit/caller.wit rename to wasm-rpc-stubgen/test-data/wit/caller-no-dep/wit/caller.wit index 83b3ad1e..a205922c 100644 --- a/wasm-rpc-stubgen/test-data/caller-no-dep-importstub/wit/caller.wit +++ b/wasm-rpc-stubgen/test-data/wit/caller-no-dep/wit/caller.wit @@ -5,7 +5,5 @@ interface api { } world caller { -//!! import test:main-stub/stub-api; - export api; } diff --git a/wasm-rpc-stubgen/test-data/direct-circular-a-same-world-name/a.wit b/wasm-rpc-stubgen/test-data/wit/direct-circular-a-same-world-name/a.wit similarity index 85% rename from wasm-rpc-stubgen/test-data/direct-circular-a-same-world-name/a.wit rename to wasm-rpc-stubgen/test-data/wit/direct-circular-a-same-world-name/a.wit index a9caeebd..384cd847 100644 --- a/wasm-rpc-stubgen/test-data/direct-circular-a-same-world-name/a.wit +++ b/wasm-rpc-stubgen/test-data/wit/direct-circular-a-same-world-name/a.wit @@ -11,6 +11,5 @@ interface api-a { } world a { -//!! import test:b-stub/stub-a; export api-a; } diff --git a/wasm-rpc-stubgen/test-data/direct-circular-a/a.wit b/wasm-rpc-stubgen/test-data/wit/direct-circular-a/a.wit similarity index 85% rename from wasm-rpc-stubgen/test-data/direct-circular-a/a.wit rename to wasm-rpc-stubgen/test-data/wit/direct-circular-a/a.wit index 8d67a44b..384cd847 100644 --- a/wasm-rpc-stubgen/test-data/direct-circular-a/a.wit +++ b/wasm-rpc-stubgen/test-data/wit/direct-circular-a/a.wit @@ -11,6 +11,5 @@ interface api-a { } world a { -//!! import test:b-stub/stub-b; export api-a; } diff --git a/wasm-rpc-stubgen/test-data/direct-circular-b-same-world-name/b.wit b/wasm-rpc-stubgen/test-data/wit/direct-circular-b-same-world-name/b.wit similarity index 89% rename from wasm-rpc-stubgen/test-data/direct-circular-b-same-world-name/b.wit rename to wasm-rpc-stubgen/test-data/wit/direct-circular-b-same-world-name/b.wit index 371f36e3..52eedbcd 100644 --- a/wasm-rpc-stubgen/test-data/direct-circular-b-same-world-name/b.wit +++ b/wasm-rpc-stubgen/test-data/wit/direct-circular-b-same-world-name/b.wit @@ -13,6 +13,5 @@ interface api-b { } world a { -//!! import test:a-stub/stub-a; export api-b; } diff --git a/wasm-rpc-stubgen/test-data/direct-circular-b/b.wit b/wasm-rpc-stubgen/test-data/wit/direct-circular-b/b.wit similarity index 85% rename from wasm-rpc-stubgen/test-data/direct-circular-b/b.wit rename to wasm-rpc-stubgen/test-data/wit/direct-circular-b/b.wit index 9ab09d95..c50beaba 100644 --- a/wasm-rpc-stubgen/test-data/direct-circular-b/b.wit +++ b/wasm-rpc-stubgen/test-data/wit/direct-circular-b/b.wit @@ -11,6 +11,5 @@ interface api-b { } world b { -//!! import test:a-stub/stub-a; export api-b; } diff --git a/wasm-rpc-stubgen/test-data/indirect-circular-a/a.wit b/wasm-rpc-stubgen/test-data/wit/indirect-circular-a/a.wit similarity index 85% rename from wasm-rpc-stubgen/test-data/indirect-circular-a/a.wit rename to wasm-rpc-stubgen/test-data/wit/indirect-circular-a/a.wit index 957cfda0..fa1c352d 100644 --- a/wasm-rpc-stubgen/test-data/indirect-circular-a/a.wit +++ b/wasm-rpc-stubgen/test-data/wit/indirect-circular-a/a.wit @@ -11,6 +11,5 @@ interface api-a { } world a { - //!! import test:b-stub/stub-b; export api-a; } diff --git a/wasm-rpc-stubgen/test-data/indirect-circular-b/b.wit b/wasm-rpc-stubgen/test-data/wit/indirect-circular-b/b.wit similarity index 86% rename from wasm-rpc-stubgen/test-data/indirect-circular-b/b.wit rename to wasm-rpc-stubgen/test-data/wit/indirect-circular-b/b.wit index a230449c..85f8fd42 100644 --- a/wasm-rpc-stubgen/test-data/indirect-circular-b/b.wit +++ b/wasm-rpc-stubgen/test-data/wit/indirect-circular-b/b.wit @@ -11,6 +11,5 @@ interface api-b { } world b { - //!! import test:c-stub/stub-c; export api-b; } diff --git a/wasm-rpc-stubgen/test-data/indirect-circular-c/c.wit b/wasm-rpc-stubgen/test-data/wit/indirect-circular-c/c.wit similarity index 65% rename from wasm-rpc-stubgen/test-data/indirect-circular-c/c.wit rename to wasm-rpc-stubgen/test-data/wit/indirect-circular-c/c.wit index 7f4ade8f..6384fd93 100644 --- a/wasm-rpc-stubgen/test-data/indirect-circular-c/c.wit +++ b/wasm-rpc-stubgen/test-data/wit/indirect-circular-c/c.wit @@ -2,11 +2,11 @@ package test:c; +// NOTE: this interface does not introduce or use any types, so it won't be part of the stub interface api-c { func-c: func(); } world c { - //!! import test:a-stub/stub-a; export api-c; } diff --git a/wasm-rpc-stubgen/test-data/wit/many-ways-to-export/deps/sub/sub.wit b/wasm-rpc-stubgen/test-data/wit/many-ways-to-export/deps/sub/sub.wit new file mode 100644 index 00000000..abb94920 --- /dev/null +++ b/wasm-rpc-stubgen/test-data/wit/many-ways-to-export/deps/sub/sub.wit @@ -0,0 +1,31 @@ +package test:sub; + +interface iface4 { + func5: func(); +} + +interface iface5 { + record rec1 { + field: f64, + } +} + +interface iface7 { + record rec1 { + field: f64, + } +} + +interface iface12 { + record rec1 { + field: f64, + } + + func12: func() -> rec1; +} + +interface iface13 { + use iface12.{rec1}; + + func13: func() -> rec1; +} \ No newline at end of file diff --git a/wasm-rpc-stubgen/test-data/wit/many-ways-to-export/deps/sub2/sub2.wit b/wasm-rpc-stubgen/test-data/wit/many-ways-to-export/deps/sub2/sub2.wit new file mode 100644 index 00000000..473ed568 --- /dev/null +++ b/wasm-rpc-stubgen/test-data/wit/many-ways-to-export/deps/sub2/sub2.wit @@ -0,0 +1,12 @@ +package test:sub2; + +// intentionally same interface name and function as in sub.wit +interface iface4 { + func5: func(); +} + +interface iface10 { + use test:sub/iface5.{rec1}; + + func10: func() -> rec1; +} \ No newline at end of file diff --git a/wasm-rpc-stubgen/test-data/wit/many-ways-to-export/main.wit b/wasm-rpc-stubgen/test-data/wit/many-ways-to-export/main.wit new file mode 100644 index 00000000..b33e3087 --- /dev/null +++ b/wasm-rpc-stubgen/test-data/wit/many-ways-to-export/main.wit @@ -0,0 +1,76 @@ +// Example of many ways to export functions, including top-level, multiple interfaces, use and import statements, etc + +package test:exports; + +interface iface1 { + func2: func(); +} + +// TODO: +/* +interface iface2 { + use iface3.{color}; + func3: func() -> color; +} +*/ + +interface iface3 { + enum color { + red, + green, + blue + } +} + +interface iface6 { + use test:sub/iface5.{rec1}; + + func6: func() -> rec1; +} + +interface iface8 { + use test:sub/iface7.{rec1}; + + func8: func() -> rec1; +} + +interface iface9 { + use iface8.{rec1}; + + func9: func() -> rec1; +} + +interface iface11 { + use test:sub2/iface10.{rec1}; + + // intentionally using the same func name + // TODO + // func9: func() -> rec1; +} + +world api { + // TODO: + // use test:sub2/iface10.{rec1}; + + // TODO: include + + export func1: func(); + export iface1; + import iface1; + // TODO: + // export iface2; + + export inline-iface: interface { + func4: func(); + } + + export test:sub/iface4; + + export iface6; + export iface8; + export iface9; + export test:sub2/iface10; + export iface11; + export test:sub/iface12; + export test:sub/iface13; +} \ No newline at end of file diff --git a/wasm-rpc-stubgen/test-data/resources/main.wit b/wasm-rpc-stubgen/test-data/wit/resources/main.wit similarity index 100% rename from wasm-rpc-stubgen/test-data/resources/main.wit rename to wasm-rpc-stubgen/test-data/wit/resources/main.wit diff --git a/wasm-rpc-stubgen/test-data/self-circular/a.wit b/wasm-rpc-stubgen/test-data/wit/self-circular/a.wit similarity index 82% rename from wasm-rpc-stubgen/test-data/self-circular/a.wit rename to wasm-rpc-stubgen/test-data/wit/self-circular/a.wit index 829af7c9..aa7fad99 100644 --- a/wasm-rpc-stubgen/test-data/self-circular/a.wit +++ b/wasm-rpc-stubgen/test-data/wit/self-circular/a.wit @@ -11,6 +11,5 @@ interface api-a { } world a { - //!! import test:a-stub/stub-a; export api-a; } diff --git a/wasm-rpc-stubgen/tests-integration/Cargo.toml b/wasm-rpc-stubgen/tests-integration/Cargo.toml index 5d73f2c0..6592be9d 100644 --- a/wasm-rpc-stubgen/tests-integration/Cargo.toml +++ b/wasm-rpc-stubgen/tests-integration/Cargo.toml @@ -12,6 +12,10 @@ name = "wasm_rpc_stubgen_tests_integration" path = "src/lib.rs" harness = false +[[test]] +name = "app" +harness = false + [[test]] name = "compose" harness = false @@ -21,6 +25,7 @@ name = "stub_wasm" harness = false [dependencies] +assert2 = { workspace = true} fs_extra = { workspace = true } golem-wasm-ast = { workspace = true } golem-wasm-rpc-stubgen = { path = "../../wasm-rpc-stubgen", version = "0.0.0" } diff --git a/wasm-rpc-stubgen/tests-integration/tests/app.rs b/wasm-rpc-stubgen/tests-integration/tests/app.rs new file mode 100644 index 00000000..83dc39e8 --- /dev/null +++ b/wasm-rpc-stubgen/tests-integration/tests/app.rs @@ -0,0 +1,22 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use test_r::test; + +test_r::enable!(); + +#[test] +async fn rust_project() { + // TODO +} diff --git a/wasm-rpc-stubgen/tests-integration/tests/compose.rs b/wasm-rpc-stubgen/tests-integration/tests/compose.rs index b932b223..c14c6449 100644 --- a/wasm-rpc-stubgen/tests-integration/tests/compose.rs +++ b/wasm-rpc-stubgen/tests-integration/tests/compose.rs @@ -19,12 +19,12 @@ use test_r::test; use fs_extra::dir::CopyOptions; -use golem_wasm_ast::component::{Component, ComponentExternName}; -use golem_wasm_ast::{DefaultAst, IgnoreAllButMetadata}; +use golem_wasm_ast::component::Component; +use golem_wasm_ast::DefaultAst; use golem_wasm_rpc_stubgen::commands::composition::compose; use golem_wasm_rpc_stubgen::commands::dependencies::{add_stub_dependency, UpdateCargoToml}; use golem_wasm_rpc_stubgen::commands::generate::generate_and_build_stub; -use golem_wasm_rpc_stubgen::stub::StubDefinition; +use golem_wasm_rpc_stubgen::stub::{StubConfig, StubDefinition}; use std::path::{Path, PathBuf}; use tempfile::TempDir; use wasm_rpc_stubgen_tests_integration::{test_data_path, wasm_rpc_override}; @@ -33,25 +33,16 @@ test_r::enable!(); #[test] async fn compose_with_single_stub() { - let (stub_dir, stub_wasm) = init_stub("all-wit-types").await; + let (_source_dir, stub_dir, stub_wasm) = init_stub("all-wit-types").await; let caller_dir = init_caller("caller-no-dep-importstub"); add_stub_dependency( &stub_dir.path().join("wit"), &caller_dir.path().join("wit"), - false, UpdateCargoToml::Update, ) .unwrap(); - // TODO: these won't be necessary after implementing https://github.com/golemcloud/wasm-rpc/issues/66 - uncomment_imports(&caller_dir.path().join("wit/caller.wit")); - - println!( - "{}", - std::fs::read_to_string(stub_dir.path().join("wit/_stub.wit")).unwrap() - ); - compile_rust(caller_dir.path()); let component_wasm = caller_dir @@ -68,65 +59,39 @@ async fn compose_with_single_stub() { compose(&component_wasm, &[stub_wasm], &dest_wasm) .await .unwrap(); - - assert_not_importing(&dest_wasm, "test:main-stub/stub-api"); } -#[test] -async fn compose_with_single_stub_not_importing_stub() { - let (stub_dir, stub_wasm) = init_stub("all-wit-types").await; - let caller_dir = init_caller("caller-no-dep"); +async fn init_stub(name: &str) -> (TempDir, TempDir, PathBuf) { + let source_dir = TempDir::new().unwrap(); + let source_wit_root = source_dir.path().canonicalize().unwrap(); - add_stub_dependency( - &stub_dir.path().join("wit"), - &caller_dir.path().join("wit"), - false, - UpdateCargoToml::NoUpdate, + fs_extra::dir::copy( + test_data_path().join("wit").join(name), + &source_wit_root, + &CopyOptions::new().content_only(true), ) .unwrap(); - compile_rust(caller_dir.path()); - - let component_wasm = caller_dir - .path() - .join("target") - .join("wasm32-wasi") - .join("debug") - .join("caller_no_dep.wasm"); - - assert_is_component(&stub_wasm); - assert_is_component(&component_wasm); - - let dest_wasm = caller_dir.path().join("target/result.wasm"); - compose(&component_wasm, &[stub_wasm], &dest_wasm) - .await - .unwrap(); - - assert_not_importing(&dest_wasm, "test:main-stub/stub-api"); -} - -async fn init_stub(name: &str) -> (TempDir, PathBuf) { - let tempdir = TempDir::new().unwrap(); - - let source_wit_root = test_data_path().join(name); - let canonical_target_root = tempdir.path().canonicalize().unwrap(); - - let def = StubDefinition::new( - &source_wit_root, - &canonical_target_root, - &None, - "1.0.0", - &wasm_rpc_override(), - false, - ) + let stub_dir = TempDir::new().unwrap(); + let canonical_target_root = stub_dir.path().canonicalize().unwrap(); + + let def = StubDefinition::new(StubConfig { + source_wit_root, + target_root: canonical_target_root, + selected_world: None, + stub_crate_version: "1.0.0".to_string(), + wasm_rpc_override: wasm_rpc_override(), + extract_source_interface_package: true, + seal_cargo_workspace: true, + }) .unwrap(); - let wasm_path = generate_and_build_stub(&def).await.unwrap(); - (tempdir, wasm_path) + let wasm_path = generate_and_build_stub(&def, false).await.unwrap(); + (source_dir, stub_dir, wasm_path) } fn init_caller(name: &str) -> TempDir { let temp_dir = TempDir::new().unwrap(); - let source = test_data_path().join(name); + let source = test_data_path().join("wit").join(name); fs_extra::dir::copy( source, @@ -152,19 +117,3 @@ fn assert_is_component(wasm_path: &Path) { let _component: Component = Component::from_bytes(&std::fs::read(wasm_path).unwrap()).unwrap(); } - -fn assert_not_importing(wasm_path: &Path, import_name: &str) { - let component_bytes = std::fs::read(wasm_path).unwrap(); - let component: Component = - Component::from_bytes(&component_bytes).unwrap(); - component.imports().iter().all(|import| { - let ComponentExternName::Name(name) = &import.name; - name != import_name - }); -} - -fn uncomment_imports(path: &Path) { - let contents = std::fs::read_to_string(path).unwrap(); - let uncommented = contents.replace("//!!", ""); - std::fs::write(path, uncommented).unwrap(); -} diff --git a/wasm-rpc-stubgen/tests-integration/tests/stub_wasm.rs b/wasm-rpc-stubgen/tests-integration/tests/stub_wasm.rs index 68d1fef6..0fbc4430 100644 --- a/wasm-rpc-stubgen/tests-integration/tests/stub_wasm.rs +++ b/wasm-rpc-stubgen/tests-integration/tests/stub_wasm.rs @@ -15,6 +15,7 @@ //! Tests in this module are verifying the STUB WASM created by the stub generator //! regardless of how the actual wasm generator is implemented. (Currently generates Rust code and compiles it) +use fs_extra::dir::CopyOptions; use test_r::test; use golem_wasm_ast::analysis::analysed_type::*; @@ -26,7 +27,7 @@ use golem_wasm_ast::analysis::{ use golem_wasm_ast::component::Component; use golem_wasm_ast::IgnoreAllButMetadata; use golem_wasm_rpc_stubgen::commands::generate::generate_and_build_stub; -use golem_wasm_rpc_stubgen::stub::StubDefinition; +use golem_wasm_rpc_stubgen::stub::{StubConfig, StubDefinition}; use tempfile::tempdir; use wasm_rpc_stubgen_tests_integration::{test_data_path, wasm_rpc_override}; @@ -34,21 +35,31 @@ test_r::enable!(); #[test] async fn all_wit_types() { - let source_wit_root = test_data_path().join("all-wit-types"); + let source = test_data_path().join("wit/all-wit-types"); + let source_wit_root = tempdir().unwrap(); + + fs_extra::dir::copy( + source, + source_wit_root.path(), + &CopyOptions::new().content_only(true), + ) + .unwrap(); + let target_root = tempdir().unwrap(); let canonical_target_root = target_root.path().canonicalize().unwrap(); - let def = StubDefinition::new( - &source_wit_root, - &canonical_target_root, - &None, - "1.0.0", - &wasm_rpc_override(), - false, - ) + let def = StubDefinition::new(StubConfig { + source_wit_root: source_wit_root.path().to_path_buf(), + target_root: canonical_target_root, + selected_world: None, + stub_crate_version: "1.0.0".to_string(), + wasm_rpc_override: wasm_rpc_override(), + extract_source_interface_package: true, + seal_cargo_workspace: false, + }) .unwrap(); - let wasm_path = generate_and_build_stub(&def).await.unwrap(); + let wasm_path = generate_and_build_stub(&def, false).await.unwrap(); let stub_bytes = std::fs::read(wasm_path).unwrap(); let stub_component = Component::::from_bytes(&stub_bytes).unwrap(); diff --git a/wasm-rpc-stubgen/tests/add_dep.rs b/wasm-rpc-stubgen/tests/add_dep.rs index 5052648e..7f26d9d0 100644 --- a/wasm-rpc-stubgen/tests/add_dep.rs +++ b/wasm-rpc-stubgen/tests/add_dep.rs @@ -16,238 +16,229 @@ use test_r::test; +use assert2::assert; use fs_extra::dir::CopyOptions; use golem_wasm_rpc::{WASI_POLL_WIT, WASM_RPC_WIT}; -use golem_wasm_rpc_stubgen::commands::dependencies::{add_stub_dependency, UpdateCargoToml}; use golem_wasm_rpc_stubgen::commands::generate::generate_stub_wit_dir; -use golem_wasm_rpc_stubgen::stub::StubDefinition; +use golem_wasm_rpc_stubgen::stub::{StubConfig, StubDefinition}; +use golem_wasm_rpc_stubgen::wit_generate::{ + add_stub_as_dependency_to_wit_dir, AddStubAsDepConfig, UpdateCargoToml, +}; use golem_wasm_rpc_stubgen::wit_resolve::ResolvedWitDir; use golem_wasm_rpc_stubgen::WasmRpcOverride; -use std::path::Path; +use semver::Version; +use std::path::{Path, PathBuf}; use tempfile::TempDir; +use wit_encoder::{packages_from_parsed, Package, PackageName}; +use wit_parser::Resolve; test_r::enable!(); #[test] fn all_wit_types_no_collision() { - let stub_dir = init_stub("all-wit-types"); + let (_source_dir, stub_dir) = init_stub("all-wit-types"); let dest_dir = init_caller("caller-no-dep"); let stub_wit_root = stub_dir.path().join("wit"); let dest_wit_root = dest_dir.path().join("wit"); - add_stub_dependency( - &stub_wit_root, - &dest_wit_root, - false, - UpdateCargoToml::NoUpdate, - ) + add_stub_as_dependency_to_wit_dir(AddStubAsDepConfig { + stub_wit_root: stub_wit_root.clone(), + dest_wit_root: dest_wit_root.clone(), + update_cargo_toml: UpdateCargoToml::NoUpdate, + }) .unwrap(); assert_valid_wit_root(&dest_wit_root); - assert_has_wit_dep(&dest_wit_root, "io/poll.wit", WASI_POLL_WIT); - assert_has_wit_dep(&dest_wit_root, "wasm-rpc/wasm-rpc.wit", WASM_RPC_WIT); + assert_has_wasm_rpc_wit_deps(&dest_wit_root); - let stub_wit = std::fs::read_to_string(stub_wit_root.join("_stub.wit")).unwrap(); - assert_has_wit_dep(&dest_wit_root, "test_main-stub/_stub.wit", &stub_wit); + assert_has_same_wit_package( + &PackageName::new("test", "main-stub", None), + &dest_wit_root, + &stub_wit_root, + ); - let original_wit = - std::fs::read_to_string(Path::new("test-data").join("all-wit-types/main.wit")).unwrap(); - assert_has_wit_dep(&dest_wit_root, "test_main/main.wit", &original_wit); + assert_has_same_wit_package( + &PackageName::new("test", "main-interface", None), + &dest_wit_root, + &stub_wit_root, + ); } #[test] -fn all_wit_types_overwrite_protection() { - let stub_dir = init_stub("all-wit-types"); - let alternative_stub_dir = init_stub("all-wit-types-alternative"); +fn all_wit_types_re_add_with_changes() { + let (source_dir, stub_dir) = init_stub("all-wit-types"); + let (alternative_source_dir, alternative_stub_dir) = init_stub("all-wit-types-alternative"); let dest_dir = init_caller("caller-no-dep"); let stub_wit_root = stub_dir.path().join("wit"); let alternative_stub_wit_root = alternative_stub_dir.path().join("wit"); let dest_wit_root = dest_dir.path().join("wit"); - add_stub_dependency( - &stub_wit_root, - &dest_wit_root, - false, - UpdateCargoToml::NoUpdate, - ) - .unwrap(); - add_stub_dependency( - &alternative_stub_wit_root, - &dest_wit_root, - false, - UpdateCargoToml::NoUpdate, - ) + add_stub_as_dependency_to_wit_dir(AddStubAsDepConfig { + stub_wit_root: stub_wit_root.clone(), + dest_wit_root: dest_wit_root.clone(), + update_cargo_toml: UpdateCargoToml::NoUpdate, + }) .unwrap(); assert_valid_wit_root(&dest_wit_root); - - assert_has_wit_dep(&dest_wit_root, "io/poll.wit", WASI_POLL_WIT); - assert_has_wit_dep(&dest_wit_root, "wasm-rpc/wasm-rpc.wit", WASM_RPC_WIT); - - let stub_wit = std::fs::read_to_string(stub_wit_root.join("_stub.wit")).unwrap(); - assert_has_wit_dep(&dest_wit_root, "test_main-stub/_stub.wit", &stub_wit); - - let original_wit = - std::fs::read_to_string(Path::new("test-data").join("all-wit-types/main.wit")).unwrap(); - assert_has_wit_dep(&dest_wit_root, "test_main/main.wit", &original_wit); -} - -#[test] -fn all_wit_types_overwrite_protection_disabled() { - let stub_dir = init_stub("all-wit-types"); - let alternative_stub_dir = init_stub("all-wit-types-alternative"); - let dest_dir = init_caller("caller-no-dep"); - - let stub_wit_root = stub_dir.path().join("wit"); - let alternative_stub_wit_root = alternative_stub_dir.path().join("wit"); - let dest_wit_root = dest_dir.path().join("wit"); - - add_stub_dependency( + assert_has_wasm_rpc_wit_deps(&dest_wit_root); + assert_has_same_wit_package( + &PackageName::new("test", "main-interface", None), + source_dir.path(), &stub_wit_root, + ); + assert_has_same_wit_package( + &PackageName::new("test", "main-interface", None), + source_dir.path(), &dest_wit_root, - false, - UpdateCargoToml::NoUpdate, - ) - .unwrap(); - add_stub_dependency( - &alternative_stub_wit_root, + ); + assert_has_same_wit_package( + &PackageName::new("test", "main-stub", None), + &stub_wit_root, &dest_wit_root, - true, - UpdateCargoToml::NoUpdate, - ) + ); + + add_stub_as_dependency_to_wit_dir(AddStubAsDepConfig { + stub_wit_root: alternative_stub_wit_root.clone(), + dest_wit_root: dest_wit_root.clone(), + update_cargo_toml: UpdateCargoToml::NoUpdate, + }) .unwrap(); assert_valid_wit_root(&dest_wit_root); - - assert_has_wit_dep(&dest_wit_root, "io/poll.wit", WASI_POLL_WIT); - assert_has_wit_dep(&dest_wit_root, "wasm-rpc/wasm-rpc.wit", WASM_RPC_WIT); - - let stub_wit = std::fs::read_to_string(alternative_stub_wit_root.join("_stub.wit")).unwrap(); - assert_has_wit_dep(&dest_wit_root, "test_main-stub/_stub.wit", &stub_wit); - - let original_wit = - std::fs::read_to_string(Path::new("test-data").join("all-wit-types-alternative/main.wit")) - .unwrap(); - assert_has_wit_dep(&dest_wit_root, "test_main/main.wit", &original_wit); + assert_has_wasm_rpc_wit_deps(&dest_wit_root); + assert_has_same_wit_package( + &PackageName::new("test", "main-interface", None), + alternative_source_dir.path(), + &alternative_stub_wit_root, + ); + assert_has_same_wit_package( + &PackageName::new("test", "main-interface", None), + alternative_source_dir.path(), + &dest_wit_root, + ); + assert_has_same_wit_package( + &PackageName::new("test", "main-stub", None), + &alternative_stub_wit_root, + &dest_wit_root, + ); } #[test] fn many_ways_to_export_no_collision() { - let stub_dir = init_stub("many-ways-to-export"); + let (source_dir, stub_dir) = init_stub("many-ways-to-export"); let dest_dir = init_caller("caller-no-dep"); let stub_wit_root = stub_dir.path().join("wit"); let dest_wit_root = dest_dir.path().join("wit"); - add_stub_dependency( - &stub_wit_root, - &dest_wit_root, - false, - UpdateCargoToml::NoUpdate, - ) + add_stub_as_dependency_to_wit_dir(AddStubAsDepConfig { + stub_wit_root: stub_wit_root.clone(), + dest_wit_root: dest_wit_root.clone(), + update_cargo_toml: UpdateCargoToml::NoUpdate, + }) .unwrap(); assert_valid_wit_root(&dest_wit_root); - assert_has_wit_dep(&dest_wit_root, "io/poll.wit", WASI_POLL_WIT); - assert_has_wit_dep(&dest_wit_root, "wasm-rpc/wasm-rpc.wit", WASM_RPC_WIT); + assert_has_wasm_rpc_wit_deps(&dest_wit_root); - let stub_wit = std::fs::read_to_string(stub_wit_root.join("_stub.wit")).unwrap(); - assert_has_wit_dep(&dest_wit_root, "test_exports-stub/_stub.wit", &stub_wit); + assert_has_same_wit_package( + &PackageName::new("test", "exports-stub", None), + &dest_wit_root, + &stub_wit_root, + ); - let original_wit = - std::fs::read_to_string(Path::new("test-data").join("many-ways-to-export/main.wit")) - .unwrap(); - assert_has_wit_dep(&dest_wit_root, "test_exports/main.wit", &original_wit); + assert_has_same_wit_package( + &PackageName::new("test", "exports-interface", None), + source_dir.path(), + &dest_wit_root, + ); - let original_sub_wit = std::fs::read_to_string( - Path::new("test-data").join("many-ways-to-export/deps/sub/sub.wit"), - ) - .unwrap(); - assert_has_wit_dep(&dest_wit_root, "sub/sub.wit", &original_sub_wit); + assert_has_same_wit_package( + &PackageName::new("test", "sub", None), + &dest_wit_root, + Path::new("test-data/wit/many-ways-to-export/deps/sub/sub.wit"), + ); } #[test] fn direct_circular() { - let stub_a_dir = init_stub("direct-circular-a"); - let stub_b_dir = init_stub("direct-circular-b"); + let (_source_a_dir, stub_a_dir) = init_stub("direct-circular-a"); + let (_source_b_dir, stub_b_dir) = init_stub("direct-circular-b"); let dest_a = init_caller("direct-circular-a"); let dest_b = init_caller("direct-circular-b"); - add_stub_dependency( - &stub_a_dir.path().join("wit"), - dest_b.path(), - false, - UpdateCargoToml::NoUpdate, - ) + add_stub_as_dependency_to_wit_dir(AddStubAsDepConfig { + stub_wit_root: stub_a_dir.path().join("wit"), + dest_wit_root: dest_b.path().to_path_buf(), + update_cargo_toml: UpdateCargoToml::NoUpdate, + }) .unwrap(); - add_stub_dependency( - &stub_b_dir.path().join("wit"), - dest_a.path(), - false, - UpdateCargoToml::NoUpdate, - ) + add_stub_as_dependency_to_wit_dir(AddStubAsDepConfig { + stub_wit_root: stub_b_dir.path().join("wit"), + dest_wit_root: dest_a.path().to_path_buf(), + update_cargo_toml: UpdateCargoToml::NoUpdate, + }) .unwrap(); - // TODO: these won't be necessary after implementing https://github.com/golemcloud/wasm-rpc/issues/66 - uncomment_imports(&dest_a.path().join("a.wit")); - uncomment_imports(&dest_b.path().join("b.wit")); - assert_valid_wit_root(dest_a.path()); assert_valid_wit_root(dest_b.path()); - assert_has_wit_dep(dest_a.path(), "io/poll.wit", WASI_POLL_WIT); - assert_has_wit_dep(dest_a.path(), "wasm-rpc/wasm-rpc.wit", WASM_RPC_WIT); + assert_has_wasm_rpc_wit_deps(dest_a.path()); - let stub_wit_b = std::fs::read_to_string(stub_b_dir.path().join("wit/_stub.wit")).unwrap(); - assert_has_wit_dep(dest_a.path(), "test_b-stub/_stub.wit", &stub_wit_b); + assert_has_same_wit_package( + &PackageName::new("test", "b-stub", None), + dest_a.path(), + &stub_b_dir.path().join("wit"), + ); - let original_b = - std::fs::read_to_string(Path::new("test-data").join("direct-circular-b/b.wit")).unwrap(); - assert_has_wit_dep_similar(dest_a.path(), "test_b/b.wit", &original_b); + assert_has_same_wit_package( + &PackageName::new("test", "b-interface", None), + dest_a.path(), + _source_b_dir.path(), + ); - assert_has_wit_dep(dest_b.path(), "io/poll.wit", WASI_POLL_WIT); - assert_has_wit_dep(dest_b.path(), "wasm-rpc/wasm-rpc.wit", WASM_RPC_WIT); + assert_has_wasm_rpc_wit_deps(dest_b.path()); - let stub_wit_a = std::fs::read_to_string(stub_a_dir.path().join("wit/_stub.wit")).unwrap(); - assert_has_wit_dep(dest_b.path(), "test_a-stub/_stub.wit", &stub_wit_a); + assert_has_same_wit_package( + &PackageName::new("test", "a-stub", None), + dest_b.path(), + &stub_a_dir.path().join("wit"), + ); - let original_a = - std::fs::read_to_string(Path::new("test-data").join("direct-circular-a/a.wit")).unwrap(); - assert_has_wit_dep_similar(dest_b.path(), "test_a/a.wit", &original_a); + assert_has_same_wit_package( + &PackageName::new("test", "a-interface", None), + dest_b.path(), + _source_a_dir.path(), + ); } #[test] fn direct_circular_readd() { - let stub_a_dir = init_stub("direct-circular-a"); - let stub_b_dir = init_stub("direct-circular-b"); + let (_source_a_dir, stub_a_dir) = init_stub("direct-circular-a"); + let (_source_b_dir, stub_b_dir) = init_stub("direct-circular-b"); let dest_a = init_caller("direct-circular-a"); let dest_b = init_caller("direct-circular-b"); - add_stub_dependency( - &stub_a_dir.path().join("wit"), - dest_b.path(), - false, - UpdateCargoToml::NoUpdate, - ) + add_stub_as_dependency_to_wit_dir(AddStubAsDepConfig { + stub_wit_root: stub_a_dir.path().join("wit"), + dest_wit_root: dest_b.path().to_path_buf(), + update_cargo_toml: UpdateCargoToml::NoUpdate, + }) .unwrap(); - add_stub_dependency( - &stub_b_dir.path().join("wit"), - dest_a.path(), - false, - UpdateCargoToml::NoUpdate, - ) + add_stub_as_dependency_to_wit_dir(AddStubAsDepConfig { + stub_wit_root: stub_b_dir.path().join("wit"), + dest_wit_root: dest_a.path().to_path_buf(), + update_cargo_toml: UpdateCargoToml::NoUpdate, + }) .unwrap(); - // TODO: these won't be necessary after implementing https://github.com/golemcloud/wasm-rpc/issues/66 - uncomment_imports(&dest_a.path().join("a.wit")); - uncomment_imports(&dest_b.path().join("b.wit")); - assert_valid_wit_root(dest_a.path()); assert_valid_wit_root(dest_b.path()); @@ -258,176 +249,183 @@ fn direct_circular_readd() { regenerate_stub(stub_b_dir.path(), dest_b.path()); println!("Second round of add_stub_dependency calls"); - add_stub_dependency( - &stub_a_dir.path().join("wit"), - dest_b.path(), - true, - UpdateCargoToml::NoUpdate, - ) + add_stub_as_dependency_to_wit_dir(AddStubAsDepConfig { + stub_wit_root: stub_a_dir.path().join("wit"), + dest_wit_root: dest_b.path().to_path_buf(), + update_cargo_toml: UpdateCargoToml::NoUpdate, + }) .unwrap(); - add_stub_dependency( - &stub_b_dir.path().join("wit"), - dest_a.path(), - true, - UpdateCargoToml::NoUpdate, - ) + add_stub_as_dependency_to_wit_dir(AddStubAsDepConfig { + stub_wit_root: stub_b_dir.path().join("wit"), + dest_wit_root: dest_a.path().to_path_buf(), + update_cargo_toml: UpdateCargoToml::NoUpdate, + }) .unwrap(); assert_valid_wit_root(dest_a.path()); assert_valid_wit_root(dest_b.path()); - assert_has_wit_dep(dest_a.path(), "io/poll.wit", WASI_POLL_WIT); - assert_has_wit_dep(dest_a.path(), "wasm-rpc/wasm-rpc.wit", WASM_RPC_WIT); + assert_has_wasm_rpc_wit_deps(dest_a.path()); - let stub_wit_b = std::fs::read_to_string(stub_b_dir.path().join("wit/_stub.wit")).unwrap(); - assert_has_wit_dep(dest_a.path(), "test_b-stub/_stub.wit", &stub_wit_b); + assert_has_same_wit_package( + &PackageName::new("test", "b-stub", None), + dest_a.path(), + &stub_b_dir.path().join("wit"), + ); - let original_b = - std::fs::read_to_string(Path::new("test-data").join("direct-circular-b/b.wit")).unwrap(); - assert_has_wit_dep_similar(dest_a.path(), "test_b/b.wit", &original_b); + // TODO: diff on circular import + /*assert_has_same_wit_package( + &PackageName::new("test", "b", None), + dest_a.path(), + dest_b.path(), + );*/ - assert_has_wit_dep(dest_b.path(), "io/poll.wit", WASI_POLL_WIT); - assert_has_wit_dep(dest_b.path(), "wasm-rpc/wasm-rpc.wit", WASM_RPC_WIT); + assert_has_wasm_rpc_wit_deps(dest_b.path()); - let stub_wit_a = std::fs::read_to_string(stub_a_dir.path().join("wit/_stub.wit")).unwrap(); - assert_has_wit_dep(dest_b.path(), "test_a-stub/_stub.wit", &stub_wit_a); + assert_has_same_wit_package( + &PackageName::new("test", "a-stub", None), + dest_b.path(), + &stub_a_dir.path().join("wit"), + ); - let original_a = - std::fs::read_to_string(Path::new("test-data").join("direct-circular-a/a.wit")).unwrap(); - assert_has_wit_dep_similar(dest_b.path(), "test_a/a.wit", &original_a); + // TODO: diff on circular import + /* + assert_has_same_wit_package( + &PackageName::new("test", "a", None), + dest_b.path(), + dest_a.path(), + ); + */ } #[test] fn direct_circular_same_world_name() { - let stub_a_dir = init_stub("direct-circular-a-same-world-name"); - let stub_b_dir = init_stub("direct-circular-b-same-world-name"); + let (source_a_dir, stub_a_dir) = init_stub("direct-circular-a-same-world-name"); + let (source_b_dir, stub_b_dir) = init_stub("direct-circular-b-same-world-name"); let dest_a = init_caller("direct-circular-a-same-world-name"); let dest_b = init_caller("direct-circular-b-same-world-name"); - add_stub_dependency( - &stub_a_dir.path().join("wit"), - dest_b.path(), - false, - UpdateCargoToml::NoUpdate, - ) + add_stub_as_dependency_to_wit_dir(AddStubAsDepConfig { + stub_wit_root: stub_a_dir.path().join("wit"), + dest_wit_root: dest_b.path().to_path_buf(), + update_cargo_toml: UpdateCargoToml::NoUpdate, + }) .unwrap(); - add_stub_dependency( - &stub_b_dir.path().join("wit"), - dest_a.path(), - false, - UpdateCargoToml::NoUpdate, - ) + add_stub_as_dependency_to_wit_dir(AddStubAsDepConfig { + stub_wit_root: stub_b_dir.path().join("wit"), + dest_wit_root: dest_a.path().to_path_buf(), + update_cargo_toml: UpdateCargoToml::NoUpdate, + }) .unwrap(); - // TODO: these won't be necessary after implementing https://github.com/golemcloud/wasm-rpc/issues/66 - uncomment_imports(&dest_a.path().join("a.wit")); - uncomment_imports(&dest_b.path().join("b.wit")); - assert_valid_wit_root(dest_a.path()); assert_valid_wit_root(dest_b.path()); - assert_has_wit_dep(dest_a.path(), "io/poll.wit", WASI_POLL_WIT); - assert_has_wit_dep(dest_a.path(), "wasm-rpc/wasm-rpc.wit", WASM_RPC_WIT); + assert_has_wasm_rpc_wit_deps(dest_a.path()); - let stub_wit_b = std::fs::read_to_string(stub_b_dir.path().join("wit/_stub.wit")).unwrap(); - assert_has_wit_dep(dest_a.path(), "test_b-stub/_stub.wit", &stub_wit_b); + assert_has_same_wit_package( + &PackageName::new("test", "b-stub", None), + dest_a.path(), + &stub_b_dir.path().join("wit"), + ); - let original_b = std::fs::read_to_string( - Path::new("test-data").join("direct-circular-b-same-world-name/b.wit"), - ) - .unwrap(); - assert_has_wit_dep_similar(dest_a.path(), "test_b/b.wit", &original_b); + assert_has_same_wit_package( + &PackageName::new("test", "b-interface", None), + dest_a.path(), + source_b_dir.path(), + ); - assert_has_wit_dep(dest_b.path(), "io/poll.wit", WASI_POLL_WIT); - assert_has_wit_dep(dest_b.path(), "wasm-rpc/wasm-rpc.wit", WASM_RPC_WIT); + assert_has_wasm_rpc_wit_deps(dest_b.path()); - let stub_wit_a = std::fs::read_to_string(stub_a_dir.path().join("wit/_stub.wit")).unwrap(); - assert_has_wit_dep(dest_b.path(), "test_a-stub/_stub.wit", &stub_wit_a); + assert_has_same_wit_package( + &PackageName::new("test", "a-stub", None), + dest_b.path(), + &stub_a_dir.path().join("wit"), + ); - let original_a = std::fs::read_to_string( - Path::new("test-data").join("direct-circular-a-same-world-name/a.wit"), - ) - .unwrap(); - assert_has_wit_dep_similar(dest_b.path(), "test_a/a.wit", &original_a); + assert_has_same_wit_package( + &PackageName::new("test", "a-interface", None), + dest_b.path(), + source_a_dir.path(), + ); } #[test] fn indirect_circular() { - let stub_a_dir = init_stub("indirect-circular-a"); - let stub_b_dir = init_stub("indirect-circular-b"); - let stub_c_dir = init_stub("indirect-circular-c"); + let (source_a_dir, stub_a_dir) = init_stub("indirect-circular-a"); + let (_source_b_dir, stub_b_dir) = init_stub("indirect-circular-b"); + let (_source_c_dir, stub_c_dir) = init_stub("indirect-circular-c"); let dest_a = init_caller("indirect-circular-a"); let dest_b = init_caller("indirect-circular-b"); let dest_c = init_caller("indirect-circular-c"); - add_stub_dependency( - &stub_a_dir.path().join("wit"), - dest_c.path(), - false, - UpdateCargoToml::NoUpdate, - ) + add_stub_as_dependency_to_wit_dir(AddStubAsDepConfig { + stub_wit_root: stub_a_dir.path().join("wit"), + dest_wit_root: dest_c.path().to_path_buf(), + update_cargo_toml: UpdateCargoToml::NoUpdate, + }) .unwrap(); - add_stub_dependency( - &stub_b_dir.path().join("wit"), - dest_a.path(), - false, - UpdateCargoToml::NoUpdate, - ) + add_stub_as_dependency_to_wit_dir(AddStubAsDepConfig { + stub_wit_root: stub_b_dir.path().join("wit"), + dest_wit_root: dest_a.path().to_path_buf(), + update_cargo_toml: UpdateCargoToml::NoUpdate, + }) .unwrap(); - add_stub_dependency( - &stub_c_dir.path().join("wit"), - dest_b.path(), - false, - UpdateCargoToml::NoUpdate, - ) + add_stub_as_dependency_to_wit_dir(AddStubAsDepConfig { + stub_wit_root: stub_c_dir.path().join("wit"), + dest_wit_root: dest_b.path().to_path_buf(), + update_cargo_toml: UpdateCargoToml::NoUpdate, + }) .unwrap(); - // TODO: these won't be necessary after implementing https://github.com/golemcloud/wasm-rpc/issues/66 - uncomment_imports(&dest_a.path().join("a.wit")); - uncomment_imports(&dest_b.path().join("b.wit")); - uncomment_imports(&dest_c.path().join("c.wit")); - assert_valid_wit_root(dest_a.path()); assert_valid_wit_root(dest_b.path()); assert_valid_wit_root(dest_c.path()); - assert_has_wit_dep(dest_a.path(), "io/poll.wit", WASI_POLL_WIT); - assert_has_wit_dep(dest_a.path(), "wasm-rpc/wasm-rpc.wit", WASM_RPC_WIT); + assert_has_wasm_rpc_wit_deps(dest_a.path()); - let stub_wit_b = std::fs::read_to_string(stub_b_dir.path().join("wit/_stub.wit")).unwrap(); - assert_has_wit_dep(dest_a.path(), "test_b-stub/_stub.wit", &stub_wit_b); + assert_has_same_wit_package( + &PackageName::new("test", "b-stub", None), + dest_a.path(), + &stub_b_dir.path().join("wit"), + ); - let original_b = - std::fs::read_to_string(Path::new("test-data").join("indirect-circular-b/b.wit")).unwrap(); - assert_has_wit_dep_similar(dest_a.path(), "test_b/b.wit", &original_b); + assert_has_same_wit_package( + &PackageName::new("test", "b-interface", None), + dest_a.path(), + &stub_b_dir.path().join("wit"), + ); - assert_has_wit_dep(dest_b.path(), "io/poll.wit", WASI_POLL_WIT); - assert_has_wit_dep(dest_b.path(), "wasm-rpc/wasm-rpc.wit", WASM_RPC_WIT); + assert_has_wasm_rpc_wit_deps(dest_b.path()); - let stub_wit_c = std::fs::read_to_string(stub_c_dir.path().join("wit/_stub.wit")).unwrap(); - assert_has_wit_dep(dest_b.path(), "test_c-stub/_stub.wit", &stub_wit_c); + assert_has_same_wit_package( + &PackageName::new("test", "c-stub", None), + dest_b.path(), + &stub_c_dir.path().join("wit"), + ); - let original_c = - std::fs::read_to_string(Path::new("test-data").join("indirect-circular-c/c.wit")).unwrap(); - assert_has_wit_dep_similar(dest_b.path(), "test_c/c.wit", &original_c); + assert_has_wasm_rpc_wit_deps(dest_c.path()); - assert_has_wit_dep(dest_c.path(), "io/poll.wit", WASI_POLL_WIT); - assert_has_wit_dep(dest_c.path(), "wasm-rpc/wasm-rpc.wit", WASM_RPC_WIT); + assert_has_same_wit_package( + &PackageName::new("test", "a-stub", None), + dest_c.path(), + &stub_a_dir.path().join("wit"), + ); - let stub_wit_a = std::fs::read_to_string(stub_a_dir.path().join("wit/_stub.wit")).unwrap(); - assert_has_wit_dep(dest_c.path(), "test_a-stub/_stub.wit", &stub_wit_a); - let original_a = - std::fs::read_to_string(Path::new("test-data").join("indirect-circular-a/a.wit")).unwrap(); - assert_has_wit_dep_similar(dest_c.path(), "test_a/a.wit", &original_a); + assert_has_same_wit_package( + &PackageName::new("test", "a-interface", None), + dest_c.path(), + source_a_dir.path(), + ); } #[test] fn indirect_circular_readd() { - let stub_a_dir = init_stub("indirect-circular-a"); - let stub_b_dir = init_stub("indirect-circular-b"); - let stub_c_dir = init_stub("indirect-circular-c"); + let (_source_a_dir, stub_a_dir) = init_stub("indirect-circular-a"); + let (_source_b_dir, stub_b_dir) = init_stub("indirect-circular-b"); + let (_source_c_dir, stub_c_dir) = init_stub("indirect-circular-c"); let dest_a = init_caller("indirect-circular-a"); let dest_b = init_caller("indirect-circular-b"); @@ -437,33 +435,25 @@ fn indirect_circular_readd() { println!("dest_b: {:?}", dest_b.path()); println!("dest_c: {:?}", dest_c.path()); - add_stub_dependency( - &stub_a_dir.path().join("wit"), - dest_c.path(), - false, - UpdateCargoToml::NoUpdate, - ) + add_stub_as_dependency_to_wit_dir(AddStubAsDepConfig { + stub_wit_root: stub_a_dir.path().join("wit"), + dest_wit_root: dest_c.path().to_path_buf(), + update_cargo_toml: UpdateCargoToml::NoUpdate, + }) .unwrap(); - add_stub_dependency( - &stub_b_dir.path().join("wit"), - dest_a.path(), - false, - UpdateCargoToml::NoUpdate, - ) + add_stub_as_dependency_to_wit_dir(AddStubAsDepConfig { + stub_wit_root: stub_b_dir.path().join("wit"), + dest_wit_root: dest_a.path().to_path_buf(), + update_cargo_toml: UpdateCargoToml::NoUpdate, + }) .unwrap(); - add_stub_dependency( - &stub_c_dir.path().join("wit"), - dest_b.path(), - false, - UpdateCargoToml::NoUpdate, - ) + add_stub_as_dependency_to_wit_dir(AddStubAsDepConfig { + stub_wit_root: stub_c_dir.path().join("wit"), + dest_wit_root: dest_b.path().to_path_buf(), + update_cargo_toml: UpdateCargoToml::NoUpdate, + }) .unwrap(); - // TODO: these won't be necessary after implementing https://github.com/golemcloud/wasm-rpc/issues/66 - uncomment_imports(&dest_a.path().join("a.wit")); - uncomment_imports(&dest_b.path().join("b.wit")); - uncomment_imports(&dest_c.path().join("c.wit")); - assert_valid_wit_root(dest_a.path()); assert_valid_wit_root(dest_b.path()); assert_valid_wit_root(dest_c.path()); @@ -476,175 +466,239 @@ fn indirect_circular_readd() { regenerate_stub(stub_c_dir.path(), dest_c.path()); println!("Second round of add_stub_dependency calls"); - add_stub_dependency( - &stub_a_dir.path().join("wit"), - dest_c.path(), - true, - UpdateCargoToml::NoUpdate, - ) + add_stub_as_dependency_to_wit_dir(AddStubAsDepConfig { + stub_wit_root: stub_a_dir.path().join("wit"), + dest_wit_root: dest_c.path().to_path_buf(), + update_cargo_toml: UpdateCargoToml::NoUpdate, + }) .unwrap(); - add_stub_dependency( - &stub_b_dir.path().join("wit"), - dest_a.path(), - true, - UpdateCargoToml::NoUpdate, - ) + add_stub_as_dependency_to_wit_dir(AddStubAsDepConfig { + stub_wit_root: stub_b_dir.path().join("wit"), + dest_wit_root: dest_a.path().to_path_buf(), + update_cargo_toml: UpdateCargoToml::NoUpdate, + }) .unwrap(); - add_stub_dependency( - &stub_c_dir.path().join("wit"), - dest_b.path(), - true, - UpdateCargoToml::NoUpdate, - ) + add_stub_as_dependency_to_wit_dir(AddStubAsDepConfig { + stub_wit_root: stub_c_dir.path().join("wit"), + dest_wit_root: dest_b.path().to_path_buf(), + update_cargo_toml: UpdateCargoToml::NoUpdate, + }) .unwrap(); assert_valid_wit_root(dest_a.path()); assert_valid_wit_root(dest_b.path()); assert_valid_wit_root(dest_c.path()); - assert_has_wit_dep(dest_a.path(), "io/poll.wit", WASI_POLL_WIT); - assert_has_wit_dep(dest_a.path(), "wasm-rpc/wasm-rpc.wit", WASM_RPC_WIT); + assert_has_wasm_rpc_wit_deps(dest_a.path()); + + assert_has_same_wit_package( + &PackageName::new("test", "b-stub", None), + dest_a.path(), + &stub_b_dir.path().join("wit"), + ); - let stub_wit_b = std::fs::read_to_string(stub_b_dir.path().join("wit/_stub.wit")).unwrap(); - assert_has_wit_dep(dest_a.path(), "test_b-stub/_stub.wit", &stub_wit_b); + assert_has_same_wit_package( + &PackageName::new("test", "b-interface", None), + dest_a.path(), + dest_b.path(), + ); - let original_b = - std::fs::read_to_string(Path::new("test-data").join("indirect-circular-b/b.wit")).unwrap(); - assert_has_wit_dep_similar(dest_a.path(), "test_b/b.wit", &original_b); + assert_has_wasm_rpc_wit_deps(dest_b.path()); - assert_has_wit_dep(dest_b.path(), "io/poll.wit", WASI_POLL_WIT); - assert_has_wit_dep(dest_b.path(), "wasm-rpc/wasm-rpc.wit", WASM_RPC_WIT); + assert_has_same_wit_package( + &PackageName::new("test", "c-stub", None), + dest_b.path(), + &stub_c_dir.path().join("wit"), + ); - let stub_wit_c = std::fs::read_to_string(stub_c_dir.path().join("wit/_stub.wit")).unwrap(); - assert_has_wit_dep(dest_b.path(), "test_c-stub/_stub.wit", &stub_wit_c); + assert_has_no_package_by_name( + &PackageName::new("test", "c-interface", None), + dest_b.path(), + ); + assert_has_package_by_name( + &PackageName::new("test", "c-interface", None), + dest_c.path(), + ); - let original_c = - std::fs::read_to_string(Path::new("test-data").join("indirect-circular-c/c.wit")).unwrap(); - assert_has_wit_dep_similar(dest_b.path(), "test_c/c.wit", &original_c); + assert_has_wasm_rpc_wit_deps(dest_c.path()); - assert_has_wit_dep(dest_c.path(), "io/poll.wit", WASI_POLL_WIT); - assert_has_wit_dep(dest_c.path(), "wasm-rpc/wasm-rpc.wit", WASM_RPC_WIT); + assert_has_same_wit_package( + &PackageName::new("test", "a-stub", None), + dest_c.path(), + &stub_a_dir.path().join("wit"), + ); - let stub_wit_a = std::fs::read_to_string(stub_a_dir.path().join("wit/_stub.wit")).unwrap(); - assert_has_wit_dep(dest_c.path(), "test_a-stub/_stub.wit", &stub_wit_a); - let original_a = - std::fs::read_to_string(Path::new("test-data").join("indirect-circular-a/a.wit")).unwrap(); - assert_has_wit_dep_similar(dest_c.path(), "test_a/a.wit", &original_a); + assert_has_same_wit_package( + &PackageName::new("test", "a-interface", None), + dest_c.path(), + dest_a.path(), + ); + assert_has_no_package_by_name(&PackageName::new("test", "a", None), dest_c.path()); + assert_has_package_by_name(&PackageName::new("test", "a", None), dest_a.path()); } #[test] fn self_circular() { - let stub_a_dir = init_stub("self-circular"); - let inlined_stub_a_dir = init_stub_inlined("self-circular"); + let (_source_a_dir, stub_a_dir) = init_stub("self-circular"); let dest_a = init_caller("self-circular"); - add_stub_dependency( - &stub_a_dir.path().join("wit"), - dest_a.path(), - false, - UpdateCargoToml::NoUpdate, - ) + add_stub_as_dependency_to_wit_dir(AddStubAsDepConfig { + stub_wit_root: stub_a_dir.path().join("wit"), + dest_wit_root: dest_a.path().to_path_buf(), + update_cargo_toml: UpdateCargoToml::NoUpdate, + }) .unwrap(); - // TODO: these won't be necessary after implementing https://github.com/golemcloud/wasm-rpc/issues/66 - uncomment_imports(&dest_a.path().join("a.wit")); - assert_valid_wit_root(dest_a.path()); - assert_has_wit_dep(dest_a.path(), "io/poll.wit", WASI_POLL_WIT); - assert_has_wit_dep(dest_a.path(), "wasm-rpc/wasm-rpc.wit", WASM_RPC_WIT); + assert_has_wasm_rpc_wit_deps(dest_a.path()); - let inlined_stub_wit_a = - std::fs::read_to_string(inlined_stub_a_dir.path().join("wit/_stub.wit")).unwrap(); - assert_has_wit_dep(dest_a.path(), "test_a-stub/_stub.wit", &inlined_stub_wit_a); -} - -fn init_stub(name: &str) -> TempDir { - init_stub_internal(name, false) + assert_has_same_wit_package( + &PackageName::new("test", "a-stub", None), + dest_a.path(), + &stub_a_dir.path().join("wit"), + ); } -fn init_stub_inlined(name: &str) -> TempDir { - init_stub_internal(name, true) -} +fn init_stub(name: &str) -> (TempDir, TempDir) { + let source = TempDir::new().unwrap(); + let canonical_source = source.path().canonicalize().unwrap(); -fn init_stub_internal(name: &str, always_inline_types: bool) -> TempDir { - let tempdir = TempDir::new().unwrap(); - let canonical_target_root = tempdir.path().canonicalize().unwrap(); - - let source_wit_root = Path::new("test-data").join(name); - let def = StubDefinition::new( - &source_wit_root, - &canonical_target_root, - &None, - "1.0.0", - &WasmRpcOverride::default(), - always_inline_types, + fs_extra::dir::copy( + Path::new("test-data/wit").join(name), + &canonical_source, + &CopyOptions::new().content_only(true), ) .unwrap(); + + let target = TempDir::new().unwrap(); + let canonical_target = target.path().canonicalize().unwrap(); + + let def = StubDefinition::new(StubConfig { + source_wit_root: canonical_source, + target_root: canonical_target, + selected_world: None, + stub_crate_version: "1.0.0".to_string(), + wasm_rpc_override: WasmRpcOverride::default(), + extract_source_interface_package: true, + seal_cargo_workspace: false, + }) + .unwrap(); let _ = generate_stub_wit_dir(&def).unwrap(); - tempdir + (source, target) } fn regenerate_stub(stub_dir: &Path, source_wit_root: &Path) { - let def = StubDefinition::new( - source_wit_root, - stub_dir, - &None, - "1.0.0", - &WasmRpcOverride::default(), - false, - ) + let def = StubDefinition::new(StubConfig { + source_wit_root: source_wit_root.to_path_buf(), + target_root: stub_dir.to_path_buf(), + selected_world: None, + stub_crate_version: "1.0.0".to_string(), + wasm_rpc_override: WasmRpcOverride::default(), + extract_source_interface_package: true, + seal_cargo_workspace: false, + }) .unwrap(); let _ = generate_stub_wit_dir(&def).unwrap(); } fn init_caller(name: &str) -> TempDir { - let tempdir = TempDir::new().unwrap(); - let source = Path::new("test-data").join(name); + let temp_dir = TempDir::new().unwrap(); + let source = Path::new("test-data/wit").join(name); fs_extra::dir::copy( source, - tempdir.path(), + temp_dir.path(), &CopyOptions::new().content_only(true).overwrite(true), ) .unwrap(); - tempdir + temp_dir } fn assert_valid_wit_root(wit_root: &Path) { ResolvedWitDir::new(wit_root).unwrap(); } -/// Asserts that the destination WIT root has a dependency with the given name and contents. -fn assert_has_wit_dep(wit_dir: &Path, name: &str, expected_contents: &str) { - let wit_file = wit_dir.join("deps").join(name); - let contents = std::fs::read_to_string(&wit_file) - .unwrap_or_else(|_| panic!("Could not find {wit_file:?}")); - assert_eq!(contents, expected_contents, "checking {wit_file:?}"); +trait WitSource { + fn resolve(&self) -> anyhow::Result; + + fn encoded_packages(&self) -> anyhow::Result> { + Ok(packages_from_parsed(&self.resolve()?)) + } + + fn encoded_package(&self, package_name: &PackageName) -> anyhow::Result { + self.encoded_packages()? + .into_iter() + .find(|package| package.name() == package_name) + .ok_or_else(|| anyhow::anyhow!("package {} not found", package_name)) + } + + fn encoded_package_wit(&self, package_name: &PackageName) -> anyhow::Result { + self.encoded_package(package_name) + .map(|package| package.to_string()) + } +} + +impl WitSource for &Path { + fn resolve(&self) -> anyhow::Result { + let mut resolve = Resolve::new(); + let _ = resolve.push_path(self)?; + Ok(resolve) + } +} + +impl WitSource for &PathBuf { + fn resolve(&self) -> anyhow::Result { + let mut resolve = Resolve::new(); + let _ = resolve.push_path(self)?; + Ok(resolve) + } } -/// Asserts that the destination WIT root has a dependency with the given name and it's contents are -/// similar to the expected one - meaning each non-comment line can be found in the expected contents -/// but missing lines are allowed. -fn assert_has_wit_dep_similar(wit_dir: &Path, name: &str, expected_contents: &str) { - let wit_file = wit_dir.join("deps").join(name); - let contents = std::fs::read_to_string(&wit_file) - .unwrap_or_else(|_| panic!("Could not find {wit_file:?}")); - - for line in contents.lines() { - if !line.starts_with("//") { - assert!( - expected_contents.contains(line.trim()), - "checking {wit_file:?}, line {line}" - ); +impl WitSource for &[(&str, &str)] { + fn resolve(&self) -> anyhow::Result { + let mut resolve = Resolve::new(); + for (name, source) in *self { + let _ = resolve.push_str(name, source)?; } + Ok(resolve) } } -fn uncomment_imports(path: &Path) { - let contents = std::fs::read_to_string(path).unwrap(); - let uncommented = contents.replace("//!!", ""); - std::fs::write(path, uncommented).unwrap(); +/// Asserts that both wit sources contains the same effective (encoded) wit package. +fn assert_has_same_wit_package( + package_name: &PackageName, + actual_wit_source: impl WitSource, + expected_wit_source: impl WitSource, +) { + let actual_wit = actual_wit_source.encoded_package_wit(package_name).unwrap(); + let expected_wit = expected_wit_source + .encoded_package_wit(package_name) + .unwrap(); + assert!(actual_wit == expected_wit) +} + +fn assert_has_no_package_by_name(package_name: &PackageName, wit_source: impl WitSource) { + assert!(wit_source.encoded_package(package_name).is_err()) +} + +fn assert_has_package_by_name(package_name: &PackageName, wit_source: impl WitSource) { + assert!(wit_source.encoded_package(package_name).is_ok()) +} + +fn assert_has_wasm_rpc_wit_deps(wit_dir: &Path) { + let deps = vec![("poll", WASI_POLL_WIT), ("wasm-rpc", WASM_RPC_WIT)]; + + assert_has_same_wit_package( + &PackageName::new("wasi", "io", Some(Version::new(0, 2, 0))), + wit_dir, + deps.as_slice(), + ); + assert_has_same_wit_package( + &PackageName::new("golem", "rpc", Some(Version::new(0, 1, 0))), + wit_dir, + deps.as_slice(), + ); } diff --git a/wasm-rpc-stubgen/tests/wit.rs b/wasm-rpc-stubgen/tests/wit.rs index 47e045ce..c0f637ed 100644 --- a/wasm-rpc-stubgen/tests/wit.rs +++ b/wasm-rpc-stubgen/tests/wit.rs @@ -16,28 +16,30 @@ use test_r::test; +use fs_extra::dir::CopyOptions; use golem_wasm_rpc_stubgen::commands::generate::generate_stub_wit_dir; -use golem_wasm_rpc_stubgen::stub::StubDefinition; +use golem_wasm_rpc_stubgen::stub::{StubConfig, StubDefinition}; use golem_wasm_rpc_stubgen::WasmRpcOverride; use std::path::Path; -use tempfile::tempdir; +use tempfile::{tempdir, TempDir}; use wit_parser::{FunctionKind, Resolve, TypeDefKind, TypeOwner}; test_r::enable!(); #[test] fn all_wit_types() { - let source_wit_root = Path::new("test-data/all-wit-types"); + let source_wit_root = init_source("all-wit-types"); let target_root = tempdir().unwrap(); - let def = StubDefinition::new( - source_wit_root, - target_root.path(), - &None, - "1.0.0", - &WasmRpcOverride::default(), - false, - ) + let def = StubDefinition::new(StubConfig { + source_wit_root: source_wit_root.path().to_path_buf(), + target_root: target_root.path().to_path_buf(), + selected_world: None, + stub_crate_version: "1.0.0".to_string(), + wasm_rpc_override: WasmRpcOverride::default(), + extract_source_interface_package: true, + seal_cargo_workspace: false, + }) .unwrap(); let resolve = generate_stub_wit_dir(&def).unwrap().resolve; @@ -96,131 +98,39 @@ fn all_wit_types() { assert_has_stub_function(&resolve, "stub-api", "iface1", "get-color", true); assert_has_stub_function(&resolve, "stub-api", "iface1", "set-color", false); assert_has_stub_function(&resolve, "stub-api", "iface1", "validate-permissions", true); -} - -#[test] -fn all_wit_types_inlined() { - let source_wit_root = Path::new("test-data/all-wit-types"); - let target_root = tempdir().unwrap(); - - let def = StubDefinition::new( - source_wit_root, - target_root.path(), - &None, - "1.0.0", - &WasmRpcOverride::default(), - true, - ) - .unwrap(); - let resolve = generate_stub_wit_dir(&def).unwrap().resolve; - - assert_has_package_name(&resolve, "test:main-stub"); - assert_has_world(&resolve, "wasm-rpc-stub-api"); - assert_has_interface(&resolve, "stub-api"); - assert_has_stub_function(&resolve, "stub-api", "iface1", "no-op", false); - assert_has_stub_function(&resolve, "stub-api", "iface1", "get-bool", true); - assert_has_stub_function(&resolve, "stub-api", "iface1", "set-bool", false); - assert_has_stub_function(&resolve, "stub-api", "iface1", "identity-bool", true); - assert_has_stub_function(&resolve, "stub-api", "iface1", "identity-s8", true); - assert_has_stub_function(&resolve, "stub-api", "iface1", "identity-s16", true); - assert_has_stub_function(&resolve, "stub-api", "iface1", "identity-s32", true); - assert_has_stub_function(&resolve, "stub-api", "iface1", "identity-s64", true); - assert_has_stub_function(&resolve, "stub-api", "iface1", "identity-u8", true); - assert_has_stub_function(&resolve, "stub-api", "iface1", "identity-u16", true); - assert_has_stub_function(&resolve, "stub-api", "iface1", "identity-u32", true); - assert_has_stub_function(&resolve, "stub-api", "iface1", "identity-u64", true); - assert_has_stub_function(&resolve, "stub-api", "iface1", "identity-f32", true); - assert_has_stub_function(&resolve, "stub-api", "iface1", "identity-f64", true); - assert_has_stub_function(&resolve, "stub-api", "iface1", "identity-char", true); - assert_has_stub_function(&resolve, "stub-api", "iface1", "identity-string", true); - assert_has_stub_function(&resolve, "stub-api", "iface1", "get-orders", true); - assert_has_stub_function(&resolve, "stub-api", "iface1", "set-orders", false); - assert_has_stub_function(&resolve, "stub-api", "iface1", "apply-metadata", true); - assert_has_stub_function(&resolve, "stub-api", "iface1", "get-option-bool", true); - assert_has_stub_function(&resolve, "stub-api", "iface1", "set-option-bool", false); - assert_has_stub_function(&resolve, "stub-api", "iface1", "get-coordinates", true); - assert_has_stub_function( - &resolve, - "stub-api", - "iface1", - "get-coordinates-alias", - true, - ); - assert_has_stub_function(&resolve, "stub-api", "iface1", "set-coordinates", false); - assert_has_stub_function( - &resolve, - "stub-api", - "iface1", - "set-coordinates-alias", - false, - ); - assert_has_stub_function(&resolve, "stub-api", "iface1", "tuple-to-point", true); - assert_has_stub_function(&resolve, "stub-api", "iface1", "pt-log-error", true); - assert_has_stub_function(&resolve, "stub-api", "iface1", "validate-pt", true); - assert_has_stub_function( + assert_defines_enum(&resolve, "test:main-interface", "iface1", "color"); + assert_defines_flags(&resolve, "test:main-interface", "iface1", "permissions"); + assert_defines_record(&resolve, "test:main-interface", "iface1", "metadata"); + assert_defines_record(&resolve, "test:main-interface", "iface1", "point"); + assert_defines_record(&resolve, "test:main-interface", "iface1", "product-item"); + assert_defines_record(&resolve, "test:main-interface", "iface1", "order"); + assert_defines_record( &resolve, - "stub-api", + "test:main-interface", "iface1", - "print-checkout-result", - true, + "order-confirmation", ); - assert_has_stub_function(&resolve, "stub-api", "iface1", "get-checkout-result", true); - assert_has_stub_function(&resolve, "stub-api", "iface1", "get-color", true); - assert_has_stub_function(&resolve, "stub-api", "iface1", "set-color", false); - assert_has_stub_function(&resolve, "stub-api", "iface1", "validate-permissions", true); + assert_defines_tuple_alias(&resolve, "test:main-interface", "iface1", "point-tuple"); + assert_defines_variant(&resolve, "test:main-interface", "iface1", "checkout-result"); - assert_defines_enum(&resolve, "stub-api", "color"); - assert_defines_flags(&resolve, "stub-api", "permissions"); - assert_defines_record(&resolve, "stub-api", "metadata"); - assert_defines_record(&resolve, "stub-api", "point"); - assert_defines_record(&resolve, "stub-api", "product-item"); - assert_defines_record(&resolve, "stub-api", "order"); - assert_defines_record(&resolve, "stub-api", "order-confirmation"); - assert_defines_tuple_alias(&resolve, "stub-api", "point-tuple"); - assert_defines_variant(&resolve, "stub-api", "checkout-result"); + // TODO: duplicated rec1 types } #[test] fn many_ways_to_export() { - let source_wit_root = Path::new("test-data/many-ways-to-export"); - let target_root = tempdir().unwrap(); - - let def = StubDefinition::new( - source_wit_root, - target_root.path(), - &None, - "1.0.0", - &WasmRpcOverride::default(), - false, - ) - .unwrap(); - let resolve = generate_stub_wit_dir(&def).unwrap().resolve; - - assert_has_package_name(&resolve, "test:exports-stub"); - assert_has_world(&resolve, "wasm-rpc-stub-api"); - assert_has_interface(&resolve, "stub-api"); - - assert_has_stub_function(&resolve, "stub-api", "api", "func1", false); - assert_has_stub_function(&resolve, "stub-api", "iface1", "func2", false); - assert_has_stub_function(&resolve, "stub-api", "iface2", "func3", true); - assert_has_stub_function(&resolve, "stub-api", "inline-iface", "func4", false); - assert_has_stub_function(&resolve, "stub-api", "iface4", "func5", false); -} - -#[test] -fn many_ways_to_export_inlined() { - let source_wit_root = Path::new("test-data/many-ways-to-export"); + let source_wit_root = init_source("many-ways-to-export"); let target_root = tempdir().unwrap(); - let def = StubDefinition::new( - source_wit_root, - target_root.path(), - &None, - "1.0.0", - &WasmRpcOverride::default(), - true, - ) + let def = StubDefinition::new(StubConfig { + source_wit_root: source_wit_root.path().to_path_buf(), + target_root: target_root.path().to_path_buf(), + selected_world: None, + stub_crate_version: "1.0.0".to_string(), + wasm_rpc_override: WasmRpcOverride::default(), + extract_source_interface_package: true, + seal_cargo_workspace: false, + }) .unwrap(); let resolve = generate_stub_wit_dir(&def).unwrap().resolve; @@ -228,13 +138,14 @@ fn many_ways_to_export_inlined() { assert_has_world(&resolve, "wasm-rpc-stub-api"); assert_has_interface(&resolve, "stub-api"); - assert_has_stub_function(&resolve, "stub-api", "api", "func1", false); + assert_has_stub_function(&resolve, "stub-api", "api-inline-functions", "func1", false); assert_has_stub_function(&resolve, "stub-api", "iface1", "func2", false); - assert_has_stub_function(&resolve, "stub-api", "iface2", "func3", true); - assert_has_stub_function(&resolve, "stub-api", "inline-iface", "func4", false); + // TODO: + // assert_has_stub_function(&resolve, "stub-api", "iface2", "func3", true); + assert_has_stub_function(&resolve, "stub-api", "api-inline-iface", "func4", false); assert_has_stub_function(&resolve, "stub-api", "iface4", "func5", false); - assert_defines_enum(&resolve, "stub-api", "color"); + // TODO: add asserts for non-unique types } fn assert_has_package_name(resolve: &Resolve, package_name: &str) { @@ -313,57 +224,107 @@ fn assert_has_stub_function( } } -fn assert_defines_enum(resolve: &Resolve, interface_name: &str, enum_name: &str) { +fn assert_defines_enum( + resolve: &Resolve, + package_name: &str, + interface_name: &str, + enum_name: &str, +) { assert!(resolve .types .iter() .any(|(_, typ)| typ.name == Some(enum_name.to_string()) && matches!(typ.kind, TypeDefKind::Enum(_)) - && is_owned_by_interface(resolve, &typ.owner, interface_name))) + && is_owned_by_interface(resolve, &typ.owner, package_name, interface_name))) } -fn assert_defines_flags(resolve: &Resolve, interface_name: &str, flags_name: &str) { +fn assert_defines_flags( + resolve: &Resolve, + package_name: &str, + interface_name: &str, + flags_name: &str, +) { assert!(resolve .types .iter() .any(|(_, typ)| typ.name == Some(flags_name.to_string()) && matches!(typ.kind, TypeDefKind::Flags(_)) - && is_owned_by_interface(resolve, &typ.owner, interface_name))) + && is_owned_by_interface(resolve, &typ.owner, package_name, interface_name))) } -fn assert_defines_record(resolve: &Resolve, interface_name: &str, record_name: &str) { +fn assert_defines_record( + resolve: &Resolve, + package_name: &str, + interface_name: &str, + record_name: &str, +) { assert!(resolve .types .iter() .any(|(_, typ)| typ.name == Some(record_name.to_string()) && matches!(typ.kind, TypeDefKind::Record(_)) - && is_owned_by_interface(resolve, &typ.owner, interface_name))) + && is_owned_by_interface(resolve, &typ.owner, package_name, interface_name))) } -fn assert_defines_tuple_alias(resolve: &Resolve, interface_name: &str, alias_name: &str) { +fn assert_defines_tuple_alias( + resolve: &Resolve, + package_name: &str, + interface_name: &str, + alias_name: &str, +) { assert!(resolve .types .iter() .any(|(_, typ)| typ.name == Some(alias_name.to_string()) && matches!(typ.kind, TypeDefKind::Tuple(_)) - && is_owned_by_interface(resolve, &typ.owner, interface_name))) + && is_owned_by_interface(resolve, &typ.owner, package_name, interface_name))) } -fn assert_defines_variant(resolve: &Resolve, interface_name: &str, variant_name: &str) { +fn assert_defines_variant( + resolve: &Resolve, + package_name: &str, + interface_name: &str, + variant_name: &str, +) { assert!(resolve .types .iter() .any(|(_, typ)| typ.name == Some(variant_name.to_string()) && matches!(typ.kind, TypeDefKind::Variant(_)) - && is_owned_by_interface(resolve, &typ.owner, interface_name))) + && is_owned_by_interface(resolve, &typ.owner, package_name, interface_name))) } -fn is_owned_by_interface(resolve: &Resolve, owner: &TypeOwner, interface_name: &str) -> bool { +fn is_owned_by_interface( + resolve: &Resolve, + owner: &TypeOwner, + package_name: &str, + interface_name: &str, +) -> bool { match owner { TypeOwner::World(_) => false, TypeOwner::Interface(iface_id) => { - resolve.interfaces.get(*iface_id).unwrap().name == Some(interface_name.to_string()) + let interface = resolve.interfaces.get(*iface_id).unwrap(); + interface.name == Some(interface_name.to_string()) + && interface + .package + .and_then(|package_id| resolve.packages.get(package_id)) + .map(|package| package.name.to_string()) + == Some(package_name.to_string()) } TypeOwner::None => false, } } + +fn init_source(name: &str) -> TempDir { + let temp_dir = TempDir::new().unwrap(); + let source = Path::new("test-data/wit").join(name); + + fs_extra::dir::copy( + source, + temp_dir.path(), + &CopyOptions::new().content_only(true).overwrite(true), + ) + .unwrap(); + + temp_dir +} diff --git a/wasm-rpc/src/version.rs b/wasm-rpc/src/version.rs index 6aa64d85..454223ee 100644 --- a/wasm-rpc/src/version.rs +++ b/wasm-rpc/src/version.rs @@ -3,7 +3,7 @@ pub use git_version::git_version; macro_rules! lib_version { () => {{ let version = - crate::version::git_version!(args = ["--tags"], cargo_prefix = "", fallback = "0.0.0"); + crate::version::git_version!(args = ["--tags"], cargo_prefix = "", fallback = "1.0.0"); if !version.is_empty() && version.as_bytes()[0] == b'v' { unsafe { std::str::from_utf8_unchecked(std::slice::from_raw_parts(