Skip to main content

actr_cli/commands/codegen/
proto_model.rs

1use crate::error::{ActrCliError, Result};
2use crate::utils::to_snake_case;
3use actr_config::ManifestConfig;
4use actr_protocol::ActrType;
5use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ProtoSide {
10    Local,
11    Remote,
12}
13
14#[derive(Debug, Clone)]
15pub struct ProtoModel {
16    pub files: Vec<ProtoFileModel>,
17    pub local_services: Vec<ServiceModel>,
18    pub remote_services: Vec<ServiceModel>,
19}
20
21#[derive(Debug, Clone)]
22pub struct ProtoFileModel {
23    pub proto_file: PathBuf,
24    pub relative_path: PathBuf,
25    pub package: String,
26    pub side: ProtoSide,
27    pub declared_type_names: Vec<String>,
28    pub services: Vec<ServiceModel>,
29}
30
31#[derive(Debug, Clone)]
32pub struct ServiceModel {
33    pub name: String,
34    pub package: String,
35    pub proto_file: PathBuf,
36    pub relative_path: PathBuf,
37    pub side: ProtoSide,
38    pub methods: Vec<MethodModel>,
39    pub actr_type: Option<String>,
40}
41
42#[derive(Debug, Clone)]
43pub struct MethodModel {
44    pub name: String,
45    pub snake_name: String,
46    pub input_type: String,
47    pub output_type: String,
48    pub route_key: String,
49}
50
51impl ProtoModel {
52    pub fn parse(
53        proto_files: &[PathBuf],
54        input_path: &Path,
55        config: &ManifestConfig,
56    ) -> Result<Self> {
57        let proto_root = if input_path.is_file() {
58            input_path.parent().unwrap_or_else(|| Path::new("."))
59        } else {
60            input_path
61        };
62
63        let dependency_actr_types: HashMap<String, String> = config
64            .dependencies
65            .iter()
66            .filter_map(|dependency| {
67                dependency
68                    .actr_type
69                    .as_ref()
70                    .map(|actr_type| (dependency.alias.clone(), actr_type.to_string_repr()))
71            })
72            .collect();
73
74        let default_manufacturer = config.package.actr_type.manufacturer.clone();
75
76        let mut files = Vec::new();
77        let mut local_services = Vec::new();
78        let mut remote_services = Vec::new();
79
80        for proto_file in proto_files {
81            let relative_path = proto_file
82                .strip_prefix(proto_root)
83                .unwrap_or(proto_file)
84                .to_path_buf();
85            let side = classify_proto_side(&relative_path);
86            let parsed = parse_proto_file(proto_file)?;
87
88            let remote_actr_type = if side == ProtoSide::Remote {
89                infer_remote_actr_type(
90                    &relative_path,
91                    &dependency_actr_types,
92                    &default_manufacturer,
93                    parsed.services.first().map(|service| service.name.as_str()),
94                )
95            } else {
96                None
97            };
98
99            let services: Vec<ServiceModel> = parsed
100                .services
101                .into_iter()
102                .map(|service| {
103                    let service_model = ServiceModel {
104                        name: service.name,
105                        package: parsed.package.clone(),
106                        proto_file: proto_file.clone(),
107                        relative_path: relative_path.clone(),
108                        side,
109                        methods: service.methods,
110                        actr_type: remote_actr_type.clone(),
111                    };
112
113                    if side == ProtoSide::Local {
114                        local_services.push(service_model.clone());
115                    } else {
116                        remote_services.push(service_model.clone());
117                    }
118
119                    service_model
120                })
121                .collect();
122
123            files.push(ProtoFileModel {
124                proto_file: proto_file.clone(),
125                relative_path,
126                package: parsed.package,
127                side,
128                declared_type_names: parsed.declared_type_names,
129                services,
130            });
131        }
132
133        Ok(Self {
134            files,
135            local_services,
136            remote_services,
137        })
138    }
139}
140
141#[derive(Debug)]
142struct ParsedProtoFile {
143    package: String,
144    declared_type_names: Vec<String>,
145    services: Vec<ParsedService>,
146}
147
148#[derive(Debug)]
149struct ParsedService {
150    name: String,
151    methods: Vec<MethodModel>,
152}
153
154fn classify_proto_side(relative_path: &Path) -> ProtoSide {
155    let first_component = relative_path
156        .components()
157        .next()
158        .and_then(|component| component.as_os_str().to_str());
159
160    if first_component == Some("remote") {
161        ProtoSide::Remote
162    } else {
163        ProtoSide::Local
164    }
165}
166
167fn infer_remote_actr_type(
168    relative_path: &Path,
169    dependency_actr_types: &HashMap<String, String>,
170    default_manufacturer: &str,
171    service_name: Option<&str>,
172) -> Option<String> {
173    let alias = relative_path
174        .components()
175        .nth(1)
176        .and_then(|component| component.as_os_str().to_str());
177
178    if let Some(alias) = alias
179        && let Some(actr_type) = dependency_actr_types.get(alias)
180    {
181        return Some(actr_type.clone());
182    }
183
184    service_name.map(|service_name| {
185        ActrType {
186            manufacturer: default_manufacturer.to_string(),
187            name: service_name.to_string(),
188            version: "1.0.0".to_string(),
189        }
190        .to_string_repr()
191    })
192}
193
194fn parse_proto_file(proto_file: &Path) -> Result<ParsedProtoFile> {
195    let content = std::fs::read_to_string(proto_file).map_err(|e| {
196        ActrCliError::config_error(format!(
197            "Failed to read proto file {}: {e}",
198            proto_file.display()
199        ))
200    })?;
201
202    let mut package = String::new();
203    let mut declared_type_names = Vec::new();
204    let mut current_service: Option<ParsedService> = None;
205    let mut services = Vec::new();
206
207    for raw_line in content.lines() {
208        let line = raw_line.trim();
209        if line.is_empty() || line.starts_with("//") {
210            continue;
211        }
212
213        if let Some(rest) = line.strip_prefix("package ") {
214            package = rest
215                .trim_end_matches(';')
216                .split_whitespace()
217                .next()
218                .unwrap_or_default()
219                .to_string();
220            continue;
221        }
222
223        if let Some(rest) = line.strip_prefix("service ") {
224            if let Some(service) = current_service.take() {
225                services.push(service);
226            }
227
228            let name = rest
229                .split(|character: char| character.is_whitespace() || character == '{')
230                .find(|segment| !segment.is_empty())
231                .unwrap_or_default()
232                .to_string();
233
234            if !name.is_empty() {
235                declared_type_names.push(name.clone());
236                current_service = Some(ParsedService {
237                    name,
238                    methods: Vec::new(),
239                });
240            }
241            continue;
242        }
243
244        if let Some(name) = extract_declared_type_name(line, "message ") {
245            declared_type_names.push(name);
246            continue;
247        }
248
249        if let Some(name) = extract_declared_type_name(line, "enum ") {
250            declared_type_names.push(name);
251            continue;
252        }
253
254        if let Some(rest) = line.strip_prefix("rpc ")
255            && let Some(service) = current_service.as_mut()
256        {
257            if let Some(method) = parse_rpc_method(rest, &package, &service.name) {
258                service.methods.push(method);
259            }
260            continue;
261        }
262
263        if line.starts_with('}')
264            && let Some(service) = current_service.take()
265        {
266            services.push(service);
267        }
268    }
269
270    if let Some(service) = current_service.take() {
271        services.push(service);
272    }
273
274    Ok(ParsedProtoFile {
275        package,
276        declared_type_names,
277        services,
278    })
279}
280
281fn parse_rpc_method(rest: &str, package: &str, service_name: &str) -> Option<MethodModel> {
282    let input_start = rest.find('(')?;
283    let method_name = rest[..input_start]
284        .split_whitespace()
285        .next()
286        .unwrap_or_default()
287        .to_string();
288    if method_name.is_empty() {
289        return None;
290    }
291
292    let after_input_start = &rest[input_start + 1..];
293    let input_end = after_input_start.find(')')?;
294    let input_type = normalize_proto_type(&after_input_start[..input_end]);
295
296    let returns_pos = after_input_start.find("returns")?;
297    let after_returns = &after_input_start[returns_pos + "returns".len()..];
298    let output_start = after_returns.find('(')?;
299    let output_end = after_returns[output_start + 1..].find(')')?;
300    let output_type =
301        normalize_proto_type(&after_returns[output_start + 1..output_start + 1 + output_end]);
302
303    let route_key = if package.is_empty() {
304        format!("{service_name}.{method_name}")
305    } else {
306        format!("{package}.{service_name}.{method_name}")
307    };
308
309    Some(MethodModel {
310        snake_name: to_snake_case(&method_name),
311        name: method_name,
312        input_type,
313        output_type,
314        route_key,
315    })
316}
317
318fn normalize_proto_type(raw_type: &str) -> String {
319    raw_type.trim().trim_start_matches('.').to_string()
320}
321
322fn extract_declared_type_name(line: &str, prefix: &str) -> Option<String> {
323    let rest = line.strip_prefix(prefix)?;
324    let name = rest
325        .split(|character: char| character.is_whitespace() || character == '{')
326        .find(|segment| !segment.is_empty())?;
327    Some(name.to_string())
328}