1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
use crate::error::UsageErr;
use crate::parse::cmd::SchemaCmd;
use kdl::{KdlDocument, KdlEntry, KdlNode};
use std::fmt::{Display, Formatter};
use std::fs;
use std::path::Path;
use std::str::FromStr;

#[derive(Debug, Default)]
pub struct Spec {
    pub name: String,
    pub bin: String,
    pub cmd: SchemaCmd,
}

impl Spec {
    pub fn parse_file(file: &Path) -> Result<(Spec, String), UsageErr> {
        let (spec, body) = split_script(file)?;
        let mut schema = Self::from_str(&spec)?;
        if schema.bin.is_empty() {
            schema.bin = file.file_name().unwrap().to_str().unwrap().to_string();
        }
        if schema.name.is_empty() {
            schema.name = schema.bin.clone();
        }
        Ok((schema, body))
    }
}

fn split_script(file: &Path) -> Result<(String, String), UsageErr> {
    let full = fs::read_to_string(file)?;
    let schema = full.strip_prefix("#!/usr/bin/env usage\n").unwrap_or(&full);
    let (schema, body) = schema.split_once("\n#!").unwrap_or((&schema, ""));
    let schema = schema.trim().to_string();
    let body = format!("#!{}", body);
    Ok((schema, body))
}

impl FromStr for Spec {
    type Err = UsageErr;
    fn from_str(input: &str) -> Result<Spec, UsageErr> {
        let kdl: KdlDocument = input
            .parse()
            .map_err(|err: kdl::KdlError| UsageErr::KdlError(err))?;
        let mut schema = Self {
            ..Default::default()
        };
        for node in kdl.nodes() {
            match node.name().to_string().as_str() {
                "name" => schema.name = node.entries()[0].value().as_string().unwrap().to_string(),
                "bin" => schema.bin = node.entries()[0].value().as_string().unwrap().to_string(),
                "arg" => schema.cmd.args.push(node.try_into()?),
                "flag" => schema.cmd.flags.push(node.try_into()?),
                "cmd" => {
                    let node: SchemaCmd = node.try_into()?;
                    schema.cmd.subcommands.insert(node.name.to_string(), node);
                }
                _ => Err(UsageErr::InvalidInput(
                    node.to_string(),
                    *node.span(),
                    input.to_string(),
                ))?,
            }
        }
        Ok(schema)
    }
}

impl Display for Spec {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        let mut doc = KdlDocument::new();
        let nodes = &mut doc.nodes_mut();
        if !self.name.is_empty() {
            let mut node = KdlNode::new("name");
            node.push(KdlEntry::new(self.name.clone()));
            nodes.push(node);
        }
        if !self.bin.is_empty() {
            let mut node = KdlNode::new("bin");
            node.push(KdlEntry::new(self.bin.clone()));
            nodes.push(node);
        }
        for flag in self.cmd.flags.iter() {
            nodes.push(flag.into());
        }
        for arg in self.cmd.args.iter() {
            nodes.push(arg.into());
        }
        for cmd in self.cmd.subcommands.values() {
            nodes.push(cmd.into())
        }
        write!(f, "{}", doc)
    }
}

#[cfg(feature = "clap")]
impl From<&clap::Command> for Spec {
    fn from(cmd: &clap::Command) -> Self {
        Spec {
            bin: cmd.get_bin_name().unwrap_or_default().to_string(),
            name: cmd.get_name().to_string(),
            cmd: cmd.into(),
        }
    }
}

#[cfg(feature = "clap")]
impl From<&Spec> for clap::Command {
    fn from(schema: &Spec) -> Self {
        let mut cmd = clap::Command::new(&schema.name);
        for flag in schema.cmd.flags.iter() {
            cmd = cmd.arg(flag);
        }
        for arg in schema.cmd.args.iter() {
            let a = clap::Arg::new(&arg.name).required(arg.required);
            cmd = cmd.arg(a);
        }
        for scmd in schema.cmd.subcommands.values() {
            cmd = cmd.subcommand(scmd);
        }
        cmd
    }
}

#[cfg(feature = "clap")]
impl From<clap::Command> for Spec {
    fn from(cmd: clap::Command) -> Self {
        (&cmd).into()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_display() {
        let spec: Spec = r#"
name "Usage CLI"
bin "usage"
arg "arg1"
flag "-f,--force" global=true
cmd "config" {
  cmd "set" {
    arg "key" "Key to set"
    arg "value"
  }
}
        "#
        .parse()
        .unwrap();
        assert_display_snapshot!(spec, @r###"
        name "Usage CLI"
        bin "usage"
        flag "-f,--force" global=true
        arg "arg1" required=false
        cmd "config" {
            cmd "set" {
                arg "key" required=false
                arg "value" required=false
            }
        }
        "###);
    }

    #[test]
    #[cfg(feature = "clap")]
    fn test_clap() {
        let cmd = clap::Command::new("test");
        assert_display_snapshot!(Spec::from(&cmd), @r###"
        name "test"
        "###);
    }
}