Skip to main content

actr_web_protoc_codegen/
request.rs

1//! Plugin request/response types for CLI → plugin communication.
2//!
3//! The CLI serializes a `WebCodegenRequest` as JSON to stdin, and the plugin
4//! writes a `WebCodegenResponse` as JSON to stdout.
5
6use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8
9/// Complete request from CLI to plugin
10#[derive(Debug, Serialize, Deserialize)]
11pub struct WebCodegenRequest {
12    /// Path to actr.toml (used for reading raw TOML extras)
13    pub config_path: PathBuf,
14    /// Output directory for generated TS files (e.g. src/generated)
15    pub output_dir: PathBuf,
16    /// Project root directory (parent of src/)
17    pub project_root: PathBuf,
18    /// Whether already-existing user code may be overwritten
19    pub overwrite_user_code: bool,
20
21    // ── Package info ──
22    pub package_name: String,
23    pub manufacturer: String,
24    pub actr_name: String,
25    #[serde(default)]
26    pub version: String,
27    pub description: String,
28    pub authors: Vec<String>,
29    pub license: String,
30    pub tags: Vec<String>,
31
32    // ── System config ──
33    pub signaling_url: String,
34    pub realm_id: u32,
35    pub visible_in_discovery: bool,
36
37    /// AIS (Actor Identity Service) HTTP endpoint
38    #[serde(default)]
39    pub ais_endpoint: String,
40
41    /// Whether to force relay (TURN-only) for ICE transport
42    #[serde(default)]
43    pub force_relay: bool,
44
45    // ── Dependencies ──
46    pub dependencies: Vec<DependencyInfo>,
47
48    // ── WebRTC ──
49    pub stun_urls: Vec<String>,
50    pub turn_urls: Vec<String>,
51
52    // ── Observability ──
53    pub observability: ObservabilityInfo,
54
55    // ── Raw TOML (for edition, platform.web, acl, etc.) ──
56    pub raw_toml: String,
57
58    // ── Proto model ──
59    pub local_services: Vec<ServiceInfo>,
60    pub remote_services: Vec<ServiceInfo>,
61    pub files: Vec<FileInfo>,
62}
63
64#[derive(Debug, Serialize, Deserialize)]
65pub struct DependencyInfo {
66    pub alias: String,
67    pub actr_type: Option<ActrTypeInfo>,
68}
69
70#[derive(Debug, Serialize, Deserialize)]
71pub struct ActrTypeInfo {
72    pub manufacturer: String,
73    pub name: String,
74    #[serde(default)]
75    pub version: String,
76}
77
78#[derive(Debug, Serialize, Deserialize)]
79pub struct ObservabilityInfo {
80    pub filter_level: String,
81    pub tracing_enabled: bool,
82    pub tracing_endpoint: String,
83    pub tracing_service_name: String,
84}
85
86#[derive(Debug, Serialize, Deserialize)]
87pub struct ServiceInfo {
88    pub name: String,
89    pub package: String,
90    pub relative_path: PathBuf,
91    pub methods: Vec<MethodInfo>,
92    pub actr_type: Option<String>,
93}
94
95#[derive(Debug, Serialize, Deserialize)]
96pub struct MethodInfo {
97    pub name: String,
98    pub snake_name: String,
99    pub input_type: String,
100    pub output_type: String,
101    pub route_key: String,
102}
103
104#[derive(Debug, Serialize, Deserialize)]
105pub struct FileInfo {
106    pub proto_file: PathBuf,
107    pub relative_path: PathBuf,
108    pub package: String,
109    pub is_local: bool,
110    pub declared_type_names: Vec<String>,
111}
112
113/// Response from plugin to CLI
114#[derive(Debug, Serialize, Deserialize)]
115pub struct WebCodegenResponse {
116    pub success: bool,
117    pub generated_files: Vec<PathBuf>,
118    pub errors: Vec<String>,
119}
120
121impl WebCodegenRequest {
122    /// Whether this project exports services only and does not call out to
123    /// any remote actor. Used by the WASM scaffold generator to decide
124    /// whether to emit a service handler (provider) or a forwarding stub.
125    ///
126    /// Note: this is purely a codegen topology signal derived from the
127    /// proto model — it does NOT imply a passive connection role. Any
128    /// actor can be called by any other actor; connection initiator /
129    /// acceptor roles are negotiated per-peer at runtime.
130    pub fn is_service_provider_only(&self) -> bool {
131        !self.local_services.is_empty()
132            && self.remote_services.is_empty()
133            && self.dependencies.is_empty()
134    }
135
136    /// Get ACL allow types from raw TOML
137    pub fn get_acl_allow_types(&self) -> Vec<String> {
138        let raw_table: toml::Table = self.raw_toml.parse().unwrap_or_default();
139        let mut types = Vec::new();
140        if let Some(acl) = raw_table.get("acl") {
141            if let Some(rules) = acl.get("rules").and_then(|r| r.as_array()) {
142                for rule in rules {
143                    if let Some(rule_types) = rule.get("types").and_then(|t| t.as_array()) {
144                        for t in rule_types {
145                            if let Some(s) = t.as_str() {
146                                types.push(s.to_string());
147                            }
148                        }
149                    }
150                }
151            }
152        }
153        types
154    }
155
156    /// Get the target actr type for peer discovery (manufacturer:name:version).
157    ///
158    /// For an actor that imports a remote service, the target is the
159    /// declared dependency. For an actor that only exports services, the
160    /// target is the first ACL-allowed type — useful for RPCs initiated
161    /// from that actor side when it needs to reach a caller by type.
162    pub fn target_actr_type(&self) -> String {
163        if self.is_service_provider_only() {
164            self.get_acl_allow_types()
165                .first()
166                .cloned()
167                .unwrap_or_default()
168        } else {
169            self.dependencies
170                .first()
171                .and_then(|d| {
172                    d.actr_type
173                        .as_ref()
174                        .map(|t| format!("{}:{}:{}", t.manufacturer, t.name, t.version))
175                })
176                .unwrap_or_default()
177        }
178    }
179
180    /// Get client actr type (this actor's type) — manufacturer:name:version
181    pub fn client_actr_type(&self) -> String {
182        format!("{}:{}:{}", self.manufacturer, self.actr_name, self.version)
183    }
184
185    /// Get crate/WASM module name (snake_case)
186    pub fn wasm_module_name(&self) -> String {
187        to_snake_case(&self.package_name).replace('-', "_")
188    }
189
190    /// Get edition from raw TOML
191    pub fn edition(&self) -> i64 {
192        let raw_table: toml::Table = self.raw_toml.parse().unwrap_or_default();
193        raw_table
194            .get("edition")
195            .and_then(|v| v.as_integer())
196            .unwrap_or(1)
197    }
198
199    /// Get exports from raw TOML
200    pub fn exports_list(&self) -> Vec<String> {
201        let raw_table: toml::Table = self.raw_toml.parse().unwrap_or_default();
202        raw_table
203            .get("exports")
204            .and_then(|v| v.as_array())
205            .map(|arr| {
206                arr.iter()
207                    .filter_map(|v| v.as_str().map(String::from))
208                    .collect()
209            })
210            .unwrap_or_default()
211    }
212
213    /// Get platform.web from raw TOML
214    pub fn platform_web(&self) -> Option<toml::Value> {
215        let raw_table: toml::Table = self.raw_toml.parse().unwrap_or_default();
216        raw_table
217            .get("platform")
218            .and_then(|v| v.get("web"))
219            .cloned()
220    }
221
222    /// Get raw ACL value from TOML
223    pub fn raw_acl(&self) -> Option<toml::Value> {
224        let raw_table: toml::Table = self.raw_toml.parse().unwrap_or_default();
225        raw_table.get("acl").cloned()
226    }
227}
228
229fn to_snake_case(name: &str) -> String {
230    use heck::ToSnakeCase;
231    name.to_snake_case()
232}