greentic_bundle/setup/
legacy_formspec.rs1use anyhow::{Context, Result, bail};
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4
5use super::{FormSpec, QuestionKind, QuestionSpec};
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8pub struct SetupSpec {
9 #[serde(default)]
10 pub title: Option<String>,
11 #[serde(default)]
12 pub questions: Vec<SetupQuestion>,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub struct SetupQuestion {
17 #[serde(default)]
18 pub name: String,
19 #[serde(default = "default_kind")]
20 pub kind: String,
21 #[serde(default)]
22 pub required: bool,
23 #[serde(default)]
24 pub help: Option<String>,
25 #[serde(default)]
26 pub choices: Vec<String>,
27 #[serde(default)]
28 pub default: Option<Value>,
29 #[serde(default)]
30 pub secret: bool,
31 #[serde(default)]
32 pub title: Option<String>,
33}
34
35fn default_kind() -> String {
36 "string".to_string()
37}
38
39pub fn parse_setup_spec_value(value: Value) -> Result<SetupSpec> {
40 serde_json::from_value(value).context("parse legacy setup spec")
41}
42
43pub fn parse_setup_spec_str(raw: &str) -> Result<SetupSpec> {
44 serde_json::from_str(raw)
45 .or_else(|_| serde_yaml_bw::from_str(raw))
46 .context("parse legacy setup spec")
47}
48
49pub fn setup_spec_to_form_spec(spec: &SetupSpec, provider_id: &str) -> FormSpec {
50 let display_name = provider_id
51 .strip_prefix("messaging-")
52 .or_else(|| provider_id.strip_prefix("events-"))
53 .unwrap_or(provider_id);
54 let display_name = capitalize(display_name);
55 let title = spec
56 .title
57 .clone()
58 .unwrap_or_else(|| format!("{display_name} setup"));
59
60 FormSpec {
61 id: format!("{provider_id}-setup"),
62 title,
63 version: "1.0.0".to_string(),
64 description: Some(format!("{display_name} provider configuration")),
65 questions: spec.questions.iter().map(convert_question).collect(),
66 }
67}
68
69pub fn normalize_answer_value(question: &QuestionSpec, input: &Value) -> Result<Value> {
70 match question.kind {
71 QuestionKind::Boolean => normalize_boolean(input),
72 QuestionKind::Number => normalize_number(input),
73 QuestionKind::Enum => normalize_enum(input, &question.choices),
74 QuestionKind::String => normalize_string_like(input),
75 }
76}
77
78fn convert_question(q: &SetupQuestion) -> QuestionSpec {
79 let inferred = infer_question_kind(&q.name, &q.kind);
80 let kind = if q.kind == "string" {
81 inferred.0
82 } else {
83 explicit_kind(&q.kind)
84 };
85 let secret = q.secret || inferred.1;
86 let title = q.title.clone().unwrap_or_else(|| q.name.clone());
87
88 QuestionSpec {
89 id: q.name.clone(),
90 kind,
91 title,
92 description: q.help.clone(),
93 required: q.required,
94 choices: q.choices.clone(),
95 default_value: q.default.clone(),
96 secret,
97 }
98}
99
100fn explicit_kind(kind: &str) -> QuestionKind {
101 match kind {
102 "boolean" => QuestionKind::Boolean,
103 "number" => QuestionKind::Number,
104 "choice" | "enum" => QuestionKind::Enum,
105 _ => QuestionKind::String,
106 }
107}
108
109fn infer_question_kind(id: &str, fallback: &str) -> (QuestionKind, bool) {
110 match id {
111 "enabled" => (QuestionKind::Boolean, false),
112 id if id.ends_with("_token") || id.contains("secret") || id.contains("password") => {
113 (explicit_kind(fallback), true)
114 }
115 _ => (explicit_kind(fallback), false),
116 }
117}
118
119fn normalize_boolean(input: &Value) -> Result<Value> {
120 match input {
121 Value::Bool(flag) => Ok(Value::Bool(*flag)),
122 Value::String(text) => match text.to_ascii_lowercase().as_str() {
123 "true" | "t" | "yes" | "y" | "1" => Ok(Value::Bool(true)),
124 "false" | "f" | "no" | "n" | "0" => Ok(Value::Bool(false)),
125 _ => bail!("invalid boolean value {text}"),
126 },
127 other => bail!("invalid boolean value {other}"),
128 }
129}
130
131fn normalize_number(input: &Value) -> Result<Value> {
132 match input {
133 Value::Number(number) => Ok(Value::Number(number.clone())),
134 Value::String(text) => Ok(Value::Number(
135 text.parse::<serde_json::Number>()
136 .with_context(|| format!("invalid number value {text}"))?,
137 )),
138 other => bail!("invalid number value {other}"),
139 }
140}
141
142fn normalize_enum(input: &Value, choices: &[String]) -> Result<Value> {
143 let text = normalize_string_like(input)?;
144 let value = text.as_str().expect("string");
145 if choices.is_empty() || choices.iter().any(|choice| choice == value) {
146 Ok(text)
147 } else {
148 bail!("invalid choice {value}")
149 }
150}
151
152fn normalize_string_like(input: &Value) -> Result<Value> {
153 match input {
154 Value::String(text) => Ok(Value::String(text.clone())),
155 Value::Bool(flag) => Ok(Value::String(flag.to_string())),
156 Value::Number(number) => Ok(Value::String(number.to_string())),
157 other => bail!("invalid string value {other}"),
158 }
159}
160
161fn capitalize(input: &str) -> String {
162 let mut chars = input.chars();
163 match chars.next() {
164 Some(first) => format!("{}{}", first.to_ascii_uppercase(), chars.as_str()),
165 None => String::new(),
166 }
167}