1use serde::de::{self, DeserializeOwned, IntoDeserializer, Visitor};
52
53use crate::Error;
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum Environment {
76 Development,
78 Production,
80}
81
82impl Environment {
83 pub fn current() -> Self {
91 if cfg!(debug_assertions) { Self::Development } else { Self::Production }
92 }
93
94 pub fn as_str(self) -> &'static str {
102 match self {
103 Self::Development => "development",
104 Self::Production => "production",
105 }
106 }
107
108 pub fn is_development(self) -> bool {
116 self == Self::Development
117 }
118}
119
120pub fn from_vars<'a, T: DeserializeOwned>(vars: impl IntoIterator<Item = (&'a str, &'a str)>) -> crate::Result<T> {
148 let vars: Vec<(&str, &str)> = vars.into_iter().collect();
149 from_lookup(&|name| vars.iter().find(|(key, _)| *key == name).map(|(_, value)| (*value).to_owned()))
150}
151
152pub(crate) fn from_lookup<T: DeserializeOwned>(lookup: &dyn Fn(&str) -> Option<String>) -> crate::Result<T> {
154 T::deserialize(Vars { lookup }).map_err(|err| Error::internal(err.0))
155}
156
157fn missing(name: &str) -> String {
159 format!(
160 "Worker variable or secret `{name}` is missing. Fix: add `{name}: bindings.text(\"...\"),` to worker.env in cloudflare.config.ts, or for a secret run `ocre secrets push {name} --file .prod.vars`; in `ocre dev`, add `{name}=...` to .dev.vars"
161 )
162}
163
164#[derive(Debug)]
166struct ConfigError(String);
167
168impl std::fmt::Display for ConfigError {
169 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 f.write_str(&self.0)
171 }
172}
173
174impl std::error::Error for ConfigError {}
175
176impl de::Error for ConfigError {
177 fn custom<M: std::fmt::Display>(message: M) -> Self {
178 Self(message.to_string())
179 }
180
181 fn missing_field(field: &'static str) -> Self {
182 Self(missing(&field.to_ascii_uppercase()))
183 }
184}
185
186struct Vars<'a> {
188 lookup: &'a dyn Fn(&str) -> Option<String>,
189}
190
191impl<'de> de::Deserializer<'de> for Vars<'_> {
192 type Error = ConfigError;
193
194 fn deserialize_any<V: Visitor<'de>>(self, _visitor: V) -> Result<V::Value, ConfigError> {
195 Err(ConfigError("ocre::config reads a struct with named fields, one per variable".to_owned()))
196 }
197
198 fn deserialize_struct<V: Visitor<'de>>(
199 self,
200 _name: &'static str,
201 fields: &'static [&'static str],
202 visitor: V,
203 ) -> Result<V::Value, ConfigError> {
204 let entries: Vec<(&'static str, String)> = fields
205 .iter()
206 .filter_map(|field| {
207 let name = field.to_ascii_uppercase();
208 (self.lookup)(&name).map(|value| (*field, value))
209 })
210 .collect();
211 visitor.visit_map(Fields { entries: entries.into_iter(), value: None })
212 }
213
214 serde::forward_to_deserialize_any! {
215 bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string
216 bytes byte_buf option unit unit_struct newtype_struct seq tuple
217 tuple_struct map enum identifier ignored_any
218 }
219}
220
221struct Fields {
222 entries: std::vec::IntoIter<(&'static str, String)>,
223 value: Option<(&'static str, String)>,
224}
225
226impl<'de> de::MapAccess<'de> for Fields {
227 type Error = ConfigError;
228
229 fn next_key_seed<K: de::DeserializeSeed<'de>>(&mut self, seed: K) -> Result<Option<K::Value>, ConfigError> {
230 let Some((field, value)) = self.entries.next() else { return Ok(None) };
231 self.value = Some((field, value));
232 seed.deserialize(field.into_deserializer()).map(Some)
233 }
234
235 fn next_value_seed<V: de::DeserializeSeed<'de>>(&mut self, seed: V) -> Result<V::Value, ConfigError> {
236 let (field, value) = self.value.take().expect("serde asks for a value after its key");
237 let name = field.to_ascii_uppercase();
238 seed.deserialize(Text { text: &value, name: &name })
239 }
240}
241
242struct Text<'a> {
244 text: &'a str,
245 name: &'a str,
246}
247
248impl Text<'_> {
249 fn parse<T: std::str::FromStr>(&self, kind: &str) -> Result<T, ConfigError> {
250 self.text.trim().parse().map_err(|_| {
251 ConfigError(format!(
252 "Worker variable `{}` is `{}`, not {kind}. Fix: set it to {kind} in cloudflare.config.ts, the secret, or .dev.vars",
253 self.name, self.text
254 ))
255 })
256 }
257}
258
259macro_rules! parse_as {
260 ($($method:ident => $visit:ident, $kind:literal;)*) => {
261 $(fn $method<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, ConfigError> {
262 visitor.$visit(self.parse($kind)?)
263 })*
264 };
265}
266
267impl<'de> de::Deserializer<'de> for Text<'_> {
268 type Error = ConfigError;
269
270 fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, ConfigError> {
271 visitor.visit_string(self.text.to_owned())
272 }
273
274 parse_as! {
275 deserialize_i8 => visit_i8, "an integer";
276 deserialize_i16 => visit_i16, "an integer";
277 deserialize_i32 => visit_i32, "an integer";
278 deserialize_i64 => visit_i64, "an integer";
279 deserialize_u8 => visit_u8, "a positive integer";
280 deserialize_u16 => visit_u16, "a positive integer";
281 deserialize_u32 => visit_u32, "a positive integer";
282 deserialize_u64 => visit_u64, "a positive integer";
283 deserialize_f32 => visit_f32, "a number";
284 deserialize_f64 => visit_f64, "a number";
285 deserialize_char => visit_char, "a single character";
286 }
287
288 fn deserialize_bool<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, ConfigError> {
289 match self.text.trim().to_ascii_lowercase().as_str() {
290 "true" | "1" => visitor.visit_bool(true),
291 "false" | "0" => visitor.visit_bool(false),
292 _ => visitor.visit_bool(self.parse::<bool>("`true` or `false`")?),
293 }
294 }
295
296 fn deserialize_option<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, ConfigError> {
297 visitor.visit_some(self)
298 }
299
300 fn deserialize_newtype_struct<V: Visitor<'de>>(
301 self,
302 _name: &'static str,
303 visitor: V,
304 ) -> Result<V::Value, ConfigError> {
305 visitor.visit_newtype_struct(self)
306 }
307
308 fn deserialize_seq<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, ConfigError> {
309 let name = self.name;
310 let items = self.text.split(',').map(str::trim).filter(|item| !item.is_empty());
311 let items: Vec<Text<'_>> = items.map(|text| Text { text, name }).collect();
312 visitor.visit_seq(de::value::SeqDeserializer::new(items.into_iter()))
313 }
314
315 fn deserialize_enum<V: Visitor<'de>>(
316 self,
317 _name: &'static str,
318 _variants: &'static [&'static str],
319 visitor: V,
320 ) -> Result<V::Value, ConfigError> {
321 visitor.visit_enum(self.text.trim().into_deserializer())
322 }
323
324 serde::forward_to_deserialize_any! {
325 i128 u128 str string bytes byte_buf unit unit_struct tuple
326 tuple_struct map struct identifier ignored_any
327 }
328}
329
330impl<'de, 'a> IntoDeserializer<'de, ConfigError> for Text<'a> {
331 type Deserializer = Self;
332
333 fn into_deserializer(self) -> Self {
334 self
335 }
336}
337
338#[cfg(test)]
339#[path = "../tests/config.rs"]
340mod tests;