eoka_runner/config/
schema.rs1use super::params::{self, ParamDef, Params};
2use super::Action;
3use crate::{Error, Result};
4use serde::de::{self, MapAccess, Visitor};
5use serde::{Deserialize, Deserializer};
6use std::collections::HashMap;
7use std::fmt;
8use std::path::Path;
9
10#[derive(Debug, Clone, Deserialize)]
12pub struct Config {
13 pub name: String,
15
16 #[serde(default)]
18 pub params: HashMap<String, ParamDef>,
19
20 #[serde(default)]
22 pub browser: BrowserConfig,
23
24 pub target: TargetUrl,
26
27 #[serde(default)]
29 pub actions: Vec<Action>,
30
31 pub success: Option<SuccessCondition>,
33
34 pub on_failure: Option<OnFailure>,
36}
37
38impl Config {
39 pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
41 let content = std::fs::read_to_string(path.as_ref())?;
42 Self::parse_with_params(&content, &Params::new())
43 }
44
45 pub fn load_with_params<P: AsRef<Path>>(path: P, params: &Params) -> Result<Self> {
47 let content = std::fs::read_to_string(path.as_ref())?;
48 Self::parse_with_params(&content, params)
49 }
50
51 pub fn parse(yaml: &str) -> Result<Self> {
53 Self::parse_with_params(yaml, &Params::new())
54 }
55
56 pub fn parse_with_params(yaml: &str, params: &Params) -> Result<Self> {
58 let mut value: serde_yaml::Value = serde_yaml::from_str(yaml)?;
60
61 let defs: HashMap<String, ParamDef> = value
63 .get("params")
64 .and_then(|v| serde_yaml::from_value(v.clone()).ok())
65 .unwrap_or_default();
66
67 params::substitute_value(&mut value, params, &defs)?;
69
70 let config: Config = serde_yaml::from_value(value)?;
72 config.validate()?;
73 Ok(config)
74 }
75
76 fn validate(&self) -> Result<()> {
78 if self.name.is_empty() {
79 return Err(Error::Config("name is required".into()));
80 }
81 if self.target.url.is_empty() {
82 return Err(Error::Config("target.url is required".into()));
83 }
84 if let Some(ref success) = self.success {
85 if success.any.is_some() && success.all.is_some() {
86 return Err(Error::Config(
87 "success: specify either 'any' or 'all', not both".into(),
88 ));
89 }
90 }
91 if let Some(ref on_failure) = self.on_failure {
92 if let Some(ref retry) = on_failure.retry {
93 if retry.attempts == 0 {
94 return Err(Error::Config(
95 "on_failure.retry.attempts must be at least 1".into(),
96 ));
97 }
98 }
99 }
100 Ok(())
101 }
102}
103
104#[derive(Debug, Clone, Deserialize, Default)]
106pub struct BrowserConfig {
107 #[serde(default)]
109 pub headless: bool,
110
111 pub proxy: Option<String>,
113
114 pub user_agent: Option<String>,
116
117 pub viewport: Option<Viewport>,
119}
120
121#[derive(Debug, Clone, Deserialize)]
123pub struct Viewport {
124 pub width: u32,
125 pub height: u32,
126}
127
128#[derive(Debug, Clone, Deserialize)]
130pub struct TargetUrl {
131 pub url: String,
133}
134
135#[derive(Debug, Clone, Deserialize)]
137pub struct SuccessCondition {
138 pub any: Option<Vec<Condition>>,
140
141 pub all: Option<Vec<Condition>>,
143}
144
145#[derive(Debug, Clone)]
147pub enum Condition {
148 UrlContains(String),
149 TextContains(String),
150}
151
152impl<'de> Deserialize<'de> for Condition {
153 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
154 where
155 D: Deserializer<'de>,
156 {
157 deserializer.deserialize_map(ConditionVisitor)
158 }
159}
160
161struct ConditionVisitor;
162
163impl<'de> Visitor<'de> for ConditionVisitor {
164 type Value = Condition;
165
166 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
167 formatter.write_str("a condition map with single key (url_contains or text_contains)")
168 }
169
170 fn visit_map<M>(self, mut map: M) -> std::result::Result<Self::Value, M::Error>
171 where
172 M: MapAccess<'de>,
173 {
174 let key: String = map
175 .next_key()?
176 .ok_or_else(|| de::Error::custom("expected condition type key"))?;
177
178 match key.as_str() {
179 "url_contains" => Ok(Condition::UrlContains(map.next_value()?)),
180 "text_contains" => Ok(Condition::TextContains(map.next_value()?)),
181 other => Err(de::Error::unknown_variant(
182 other,
183 &["url_contains", "text_contains"],
184 )),
185 }
186 }
187}
188
189#[derive(Debug, Clone, Deserialize)]
191pub struct OnFailure {
192 pub screenshot: Option<String>,
194
195 pub retry: Option<RetryConfig>,
197}
198
199#[derive(Debug, Clone, Deserialize)]
201pub struct RetryConfig {
202 pub attempts: u32,
204
205 pub delay_ms: u64,
207}