use serde::{
de::{Error as _, Visitor},
Deserialize, Deserializer, Serialize,
};
use serde_content::Value;
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
enum Kind {
Job(Box<str>),
Interpretter(Box<str>),
Project(Box<str>),
Include(AnyNumberOf<Box<str>>),
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Entry<T> {
#[serde(flatten)]
kind: Kind,
#[serde(flatten)]
spec: T,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Interpretter {
#[serde(default)]
pub interpretter: Box<str>,
pub executable: Box<str>,
#[serde(default)]
pub arguments: Vec<Box<str>>,
pub feedback: Option<Box<str>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Interpretter2 {
pub interpretter: Box<str>,
pub provider: Box<str>,
#[serde(flatten)]
pub spec: Value<'static>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Job {
pub job: Option<Box<str>>,
#[serde(default)]
pub script: AnyNumberOf<Command>,
pub runs: Option<Box<str>>,
#[serde(default)]
pub when: AnyNumberOf<Rule>,
#[serde(default)]
pub then: AnyNumberOf<Trigger>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Job2 {
#[serde(default)]
pub script: AnyNumberOf<Command>,
pub interpretter: Option<Box<str>>,
#[serde(default)]
pub when: AnyNumberOf<Rule>,
#[serde(default)]
pub then: AnyNumberOf<Trigger>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Project {}
#[derive(Debug, Serialize, Deserialize)]
pub struct Provider {
provider: Box<str>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Include {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged, deny_unknown_fields)]
pub enum Trigger {
Start {
start: Reference,
#[serde(default)]
code: AnyNumberOf<Code>,
},
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged, deny_unknown_fields)]
pub enum Rule {
All {
all: Vec<Rule>,
},
Any {
any: Vec<Rule>,
},
After {
done: Reference,
#[serde(default)]
code: AnyNumberOf<Code>,
},
}
impl Rule {
pub(crate) fn to_job_rule(&self, ord: usize) -> crate::job::Rule {
match self {
Rule::All { all } => crate::job::Rule::All {
name: ord.to_string().into_boxed_str(),
rules: all
.iter()
.enumerate()
.map(|(o, r)| r.to_job_rule(o))
.collect(),
},
Rule::Any { any } => crate::job::Rule::Any {
name: ord.to_string().into_boxed_str(),
rules: any
.iter()
.enumerate()
.map(|(o, r)| r.to_job_rule(o))
.collect(),
},
Rule::After { done: after, code } => match after {
Reference::Single(target) => crate::job::Rule::After {
target: target.clone(),
codes: normalize_codes(code),
},
Reference::Multiple(vec) if vec.len() == 1 => crate::job::Rule::After {
target: vec[0].clone(),
codes: normalize_codes(code),
},
Reference::Multiple(vec) => crate::job::Rule::All {
name: ord.to_string().into(),
rules: vec
.clone()
.into_iter()
.map(|target| crate::job::Rule::After {
target,
codes: normalize_codes(code),
})
.collect(),
},
},
}
}
}
fn normalize_codes(code: &AnyNumberOf<Code>) -> Vec<crate::job::Code> {
let code_fix = |code| match code {
Code::Explicit(code) => code.into(),
Code::Range { min, max, mask } => (min, max, mask).into(),
};
code.clone().into_iter().map(code_fix).collect()
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged, deny_unknown_fields)]
pub enum Reference {
Single(Box<str>),
Multiple(Vec<Box<str>>),
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
#[serde(untagged, deny_unknown_fields)]
pub enum Code {
Explicit(usize),
Range {
min: Option<usize>,
max: Option<usize>,
mask: Option<usize>,
},
}
#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)]
#[serde(untagged, deny_unknown_fields)]
pub enum AnyNumberOf<T> {
#[default]
None,
Single(T),
Multiple(Vec<T>),
}
impl<T> IntoIterator for AnyNumberOf<T> {
type Item = T;
type IntoIter = <Vec<T> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
match self {
AnyNumberOf::None => vec![],
AnyNumberOf::Single(one) => vec![one],
AnyNumberOf::Multiple(vec) => vec,
}
.into_iter()
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged, deny_unknown_fields)]
pub enum Command {
Integer(usize),
SignedInteger(isize),
Float(f64),
Bool(bool),
String(Box<str>),
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Global {
#[serde(default)]
pub include: Vec<Box<str>>,
}
#[derive(Debug, thiserror::Error)]
pub enum InvalidEntry {
#[error("Invalid job definition for {job:?}: {source}\n{spec}")]
Job {
job: Box<str>,
spec: Box<str>,
source: serde_content::Error,
},
#[error("Invalid project definition for {project:?}: {source}\n{spec}")]
Project {
project: Box<str>,
spec: Box<str>,
source: serde_content::Error,
},
#[error("Invalid interpretter definition for {interpretter:?}: {source}\n{spec}")]
Interpretter {
interpretter: Box<str>,
spec: Box<str>,
source: serde_content::Error,
},
#[error("Invalid include definition: {source}\nincludes:\n{includes:?}\n{spec}")]
Include {
includes: Vec<Box<str>>,
spec: Box<str>,
source: serde_content::Error,
},
#[error("Invalid entry or job:\n{source}\n{source_job:?}\n{spec}")]
Document {
spec: Box<str>,
source: serde_content::Error,
source_job: serde_content::Error,
},
}
#[test]
fn interpreter_explore() {
let v = serde_yaml::from_str::<serde_content::Value>("interpretter: a\nexecutable: echo")
.expect("value");
println!("{v:#?}");
let d = serde_content::Deserializer::new(v);
let _i = Interpretter::deserialize(d).expect("interpretter");
}
#[test]
fn explore_tagging() {
use Kind::*;
let v = serde_yaml::from_str::<Value>("job: bake\nscript: ignored").expect("value");
let d = serde_content::Deserializer::new(v);
let e = Entry::<Value>::deserialize(d).expect("job");
assert!(matches!(e, Entry{kind:Job(x),..} if x.as_ref()=="bake"));
let v = serde_yaml::from_str::<Value>("include: resource").expect("value");
let d = serde_content::Deserializer::new(v);
let e = Entry::<Value>::deserialize(d).expect("include");
assert!(matches!(
e,
Entry {
kind: Include(AnyNumberOf::Single(x)),
..
} if x.as_ref()=="resource"
));
}
#[derive(Debug, Serialize)]
#[serde(untagged, deny_unknown_fields)]
pub enum Doc {
Project { project: Box<str> },
Include(Vec<Box<str>>),
Job(Job),
Interpretter(Interpretter2),
Empty,
}
impl<'de> Deserialize<'de> for Doc {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct V;
impl<'de> Visitor<'de> for V {
type Value = Doc;
fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
formatter.write_str(
"a willdo configuration document - job, interpretter, project, include...",
)
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Doc::Include(vec![v.into()]))
}
fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Doc::Include(vec![v.into()]))
}
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Doc::Include(vec![v.into()]))
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Doc::Empty)
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Doc::Empty)
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let mut includes = Vec::with_capacity(seq.size_hint().unwrap_or(1));
while let Some(item) = seq.next_element()? {
includes.push(item);
}
Ok(Doc::Include(includes))
}
fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
let _v = serde_content::ValueVisitor.visit_newtype_struct(deserializer)?;
todo!();
}
fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let v = serde::de::value::MapAccessDeserializer::new(map);
let e = Entry::<Value>::deserialize(v)?;
let spec = serde_content::Deserializer::new(e.spec.clone());
match e.kind {
Kind::Job(job) => {
let Job2 {
script,
interpretter,
when,
then,
} = Job2::deserialize(spec).map_err(|source| {
A::Error::custom(InvalidEntry::Job {
job: job.clone(),
spec: serde_yaml::to_string(&e.spec)
.expect("dbg")
.into_boxed_str(),
source,
})
})?;
Ok(Doc::Job(Job {
job: Some(job),
script,
runs: interpretter,
when,
then,
}))
}
Kind::Interpretter(interpretter) => {
let provider = Provider::deserialize(spec).map_err(|source| {
A::Error::custom(InvalidEntry::Interpretter {
interpretter: interpretter.clone(),
spec: serde_yaml::to_string(&e.spec)
.expect("dbg")
.into_boxed_str(),
source,
})
})?;
Ok(Doc::Interpretter(Interpretter2 {
interpretter,
provider: provider.provider,
spec: e.spec,
}))
}
Kind::Project(project) => {
Project::deserialize(spec).map_err(|source| {
A::Error::custom(InvalidEntry::Project {
project: project.clone(),
spec: serde_yaml::to_string(&e.spec)
.expect("dbg")
.into_boxed_str(),
source,
})
})?;
Ok(Doc::Project { project })
}
Kind::Include(includes) => {
Include::deserialize(spec).map_err(|source| {
A::Error::custom(InvalidEntry::Include {
includes: includes.clone().into_iter().collect(),
spec: serde_yaml::to_string(&e.spec)
.expect("dbg")
.into_boxed_str(),
source,
})
})?;
Ok(Doc::Include(includes.into_iter().collect()))
}
}
}
}
let v = Value::deserialize(deserializer)?;
let deserializer = serde_content::Deserializer::new(v.clone());
match deserializer.clone().deserialize_any(V) {
Ok(doc) => Ok(doc),
Err(entry) => Ok(Doc::Job(Job::deserialize(deserializer).map_err(|job| {
D::Error::custom(InvalidEntry::Document {
spec: serde_yaml::to_string(&v).expect("dbg").into_boxed_str(),
source: entry,
source_job: job,
})
})?)),
}
}
}
#[test]
fn test_doc_deserialize() {
let docs = serde_yaml::Deserializer::from_str(
r###"
job: bake
script:
- some
---
interpretter: doer
provider: subprocess
executable:
- do
---
include: other.yaml
---
include:
- other2.yaml
- other3.yaml
---
- include1.yaml
- include2.yaml
---
include3.yaml
---
# empty
---
null
---
# 1
---
# buggy: fail
"###,
);
for doc in docs {
let _doc = Doc::deserialize(doc).expect("doc");
}
}