actr_cli/
plugin_config.rs1use serde::Deserialize;
2use std::cmp::Ordering;
3use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5
6use crate::error::{ActrCliError, Result};
7
8const CONFIG_FILE_NAME: &str = ".protoc-plugin.toml";
9
10#[derive(Debug, Deserialize)]
11struct ProtocPluginFile {
12 version: Option<u32>,
13 plugins: Option<HashMap<String, String>>,
14}
15
16#[derive(Debug, Clone)]
17pub struct ProtocPluginConfig {
18 path: PathBuf,
19 plugins: HashMap<String, String>,
20}
21
22impl ProtocPluginConfig {
23 pub fn min_version(&self, plugin: &str) -> Option<&str> {
24 self.plugins.get(plugin).map(String::as_str)
25 }
26
27 pub fn path(&self) -> &Path {
28 &self.path
29 }
30}
31
32pub fn load_protoc_plugin_config(config_path: &Path) -> Result<Option<ProtocPluginConfig>> {
33 let config_dir = config_path
34 .parent()
35 .filter(|p| !p.as_os_str().is_empty())
36 .unwrap_or_else(|| Path::new("."));
37 let plugin_path = config_dir.join(CONFIG_FILE_NAME);
38 if !plugin_path.exists() {
39 return Ok(None);
40 }
41
42 let contents = std::fs::read_to_string(&plugin_path)?;
43 let parsed: ProtocPluginFile = toml::from_str(&contents).map_err(|e| {
44 ActrCliError::config_error(format!("Failed to parse {}: {e}", plugin_path.display()))
45 })?;
46
47 if let Some(version) = parsed.version
48 && version != 1
49 {
50 return Err(ActrCliError::config_error(format!(
51 "Unsupported .protoc-plugin.toml version {version} (expected 1)"
52 )));
53 }
54
55 let plugins = parsed.plugins.unwrap_or_default();
56 for (name, min_version) in &plugins {
57 if min_version.trim().is_empty() {
58 return Err(ActrCliError::config_error(format!(
59 "Minimum version for plugin '{name}' cannot be empty"
60 )));
61 }
62 if !is_valid_version_string(min_version) {
63 return Err(ActrCliError::config_error(format!(
64 "Invalid minimum version '{min_version}' for plugin '{name}'"
65 )));
66 }
67 }
68
69 Ok(Some(ProtocPluginConfig {
70 path: plugin_path,
71 plugins,
72 }))
73}
74
75pub fn compare_versions(v1: &str, v2: &str) -> Ordering {
76 let parse_version = |v: &str| -> Vec<u32> {
77 v.split('.')
78 .map(|s| s.parse::<u32>().unwrap_or(0))
79 .collect()
80 };
81
82 let v1_parts = parse_version(v1);
83 let v2_parts = parse_version(v2);
84 let max_len = v1_parts.len().max(v2_parts.len());
85 for i in 0..max_len {
86 let v1_part = v1_parts.get(i).copied().unwrap_or(0);
87 let v2_part = v2_parts.get(i).copied().unwrap_or(0);
88
89 match v1_part.cmp(&v2_part) {
90 Ordering::Equal => continue,
91 other => return other,
92 }
93 }
94
95 Ordering::Equal
96}
97
98pub fn version_is_at_least(candidate: &str, minimum: &str) -> bool {
99 compare_versions(candidate, minimum) != Ordering::Less
100}
101
102fn is_valid_version_string(value: &str) -> bool {
103 value.chars().all(|c| c.is_ascii_digit() || c == '.')
104 && value.chars().any(|c| c.is_ascii_digit())
105}