diff --git a/Cargo.lock b/Cargo.lock index 3babba7..eb31d12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,6 +25,7 @@ dependencies = [ "serde", "serde_derive", "toml", + "void", ] [[package]] @@ -69,3 +70,9 @@ name = "unicode-xid" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" diff --git a/Cargo.toml b/Cargo.toml index abcde13..7df1969 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,4 @@ edition = "2018" serde = "1" serde_derive = "1" toml = "0.5.8" +void = "1" diff --git a/src/main.rs b/src/main.rs index e384f37..378f8f9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,29 +1,124 @@ -use serde_derive::{Deserialize, Serialize}; use std::collections::HashMap; +use std::fmt; +use std::marker::PhantomData; +use std::path::PathBuf; +use std::str::FromStr; +use void::Void; -#[derive(Debug, Serialize, Deserialize)] +use serde::de::{self, MapAccess, Visitor}; +use serde::{Deserialize, Deserializer}; +use serde_derive::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize)] +#[serde(tag = "storage")] +enum Storage { + S3 { bucket: String }, + Filesystem { path: PathBuf }, +} + +#[derive(Debug, Deserialize, Serialize)] struct Config { + storage: Storage, sites: HashMap, } + +#[derive(Debug, Serialize, Deserialize)] +struct Extractor { + selector: String, + attribute: Option, +} + +// The `string_or_struct` function uses this impl to instantiate a `Build` if +// the input file contains a string and not a struct. According to the +// docker-compose.yml documentation, a string by itself represents a `Build` +// with just the `context` field set. +// +// > `build` can be specified either as a string containing a path to the build +// > context, or an object with the path specified under context and optionally +// > dockerfile and args. +impl FromStr for Extractor { + // This implementation of `from_str` can never fail, so use the impossible + // `Void` type as the error type. + type Err = Void; + + fn from_str(s: &str) -> Result { + Ok(Extractor { + selector: s.to_string(), + attribute: None, + }) + } +} + +fn string_or_struct<'de, T, D>(deserializer: D) -> Result +where + T: Deserialize<'de> + FromStr, + D: Deserializer<'de>, +{ + // This is a Visitor that forwards string types to T's `FromStr` impl and + // forwards map types to T's `Deserialize` impl. The `PhantomData` is to + // keep the compiler from complaining about T being an unused generic type + // parameter. We need T in order to know the Value type for the Visitor + // impl. + struct StringOrStruct(PhantomData T>); + + impl<'de, T> Visitor<'de> for StringOrStruct + where + T: Deserialize<'de> + FromStr, + { + type Value = T; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("string or map") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + Ok(FromStr::from_str(value).unwrap()) + } + + fn visit_map(self, map: M) -> Result + where + M: MapAccess<'de>, + { + // `MapAccessDeserializer` is a wrapper that turns a `MapAccess` + // into a `Deserializer`, allowing it to be used as the input to T's + // `Deserialize` implementation. T then deserializes itself using + // the entries from the map visitor. + Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)) + } + } + + deserializer.deserialize_any(StringOrStruct(PhantomData)) +} + #[derive(Debug, Serialize, Deserialize)] struct Site { url: String, title: String, hover: Option, - main_image: String, + #[serde(deserialize_with = "string_or_struct")] + main_image: Extractor, alternate_image: Option, } fn main() -> Result<(), Box> { let c = Config { + storage: Storage::Filesystem { + path: "/path/to/data".into(), + }, sites: vec![ ( "xkcd".to_string(), Site { url: "https://xkcd.com/1/".to_string(), - title: "title".to_string(), + title: "title".to_owned(), hover: None, - main_image: "img".to_string(), + main_image: Extractor { + selector: "img".to_owned(), + attribute: Some("src".to_owned()), + }, alternate_image: None, }, ), @@ -31,9 +126,12 @@ fn main() -> Result<(), Box> { "sinfest".to_string(), Site { url: "https://sinfest.net/view.php?date=2000-01-17".to_string(), - title: "title".to_string(), + title: "title".to_owned(), hover: None, - main_image: "img".to_string(), + main_image: Extractor { + selector: "img".to_owned(), + attribute: Some("src".to_owned()), + }, alternate_image: None, }, ), @@ -41,11 +139,24 @@ fn main() -> Result<(), Box> { .into_iter() .collect(), }; + println!("About to serialize"); let s = toml::to_string(&c)?; - println!("s {}", s); + println!("Serialized\n{}", s); // c is inferred as the unit type judging by the error you get when you print with Display // formatting (i.e. "{}") instead of debug formating (i.e. "{:?}"). - let c = toml::from_str(&s)?; - println!("c {:?}", c); + let c: Config = toml::from_str(&s)?; + println!("Deserialized\n{:#?}", c); + let c: Config = toml::from_str( + r#" +[storage] +storage = "Filesystem" +path = "/path/to/data" +[sites.test] +url = "http://google.com" +title = "title" +main_image = "img" + "#, + )?; + println!("Hardcode\n{:#?}", c); Ok(()) }