Skip to main content

usage/go/
types.rs

1//! Explicit, generator-only Go field types. The portable grammar remains textual.
2
3use std::collections::BTreeMap;
4use std::str::FromStr;
5
6use super::Emitted;
7
8/// An opt-in Go type for a value-taking flag or positional argument.
9/// Variadic entries become slices of this type. Absent entries keep their zero value.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ValueType {
12    /// Go's machine-sized signed integer.
13    Int,
14    /// A signed 64-bit integer.
15    Int64,
16    /// An unsigned 64-bit integer.
17    Uint64,
18    /// A 64-bit floating point value.
19    Float64,
20    /// A boolean text value (not a valueless boolean flag).
21    Bool,
22    /// A duration in Go's notation, such as `1m30s`.
23    Duration,
24}
25
26impl FromStr for ValueType {
27    type Err = String;
28
29    /// Parse the stable command-line spelling for a supported generated Go type.
30    fn from_str(value: &str) -> Result<Self, Self::Err> {
31        match value {
32            "int" => Ok(Self::Int),
33            "int64" => Ok(Self::Int64),
34            "uint64" => Ok(Self::Uint64),
35            "float64" => Ok(Self::Float64),
36            "bool" => Ok(Self::Bool),
37            "duration" => Ok(Self::Duration),
38            _ => Err(format!("unknown Go field type {value:?}; expected int, int64, uint64, float64, bool, or duration")),
39        }
40    }
41}
42
43impl ValueType {
44    /// Spell the scalar or slice type in a generated struct.
45    pub(super) fn field_type(self, many: bool) -> String {
46        let scalar = match self {
47            Self::Int => "int",
48            Self::Int64 => "int64",
49            Self::Uint64 => "uint64",
50            Self::Float64 => "float64",
51            Self::Bool => "bool",
52            Self::Duration => "time.Duration",
53        };
54        if many {
55            format!("[]{scalar}")
56        } else {
57            scalar.to_string()
58        }
59    }
60
61    /// Reuse the runtime converter rather than generating a second conversion policy.
62    pub(super) fn converter(self) -> &'static str {
63        match self {
64            Self::Int => "NativeInt",
65            Self::Int64 => "Int",
66            Self::Uint64 => "Uint",
67            Self::Float64 => "Float",
68            Self::Bool => "Bool",
69            Self::Duration => "Duration",
70        }
71    }
72}
73
74/// Reject misspelled keys and entries that have no textual values to convert.
75pub(super) fn validate(
76    commands: &[Emitted],
77    types: &BTreeMap<String, ValueType>,
78) -> Result<(), String> {
79    for key in types.keys() {
80        let flag = commands
81            .iter()
82            .flat_map(|e| &e.flags)
83            .find(|(_, named)| &named.key == key);
84        if let Some((flag, _)) = flag {
85            if flag.count || flag.arg.is_none() {
86                return Err(format!("{key} is not a value-taking flag"));
87            }
88            continue;
89        }
90        if commands
91            .iter()
92            .flat_map(|e| &e.args)
93            .any(|(_, named)| &named.key == key)
94        {
95            continue;
96        }
97        return Err(format!("{key} is not a generated command-level flag or argument key; clause fields are not supported yet"));
98    }
99    Ok(())
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use crate::go::{generate, generate_with_types, GoOptions};
106    use crate::Spec;
107
108    /// Typed generation is opt-in and invalid bindings fail before source is emitted.
109    #[test]
110    fn explicit_types_are_checked() {
111        let spec: Spec = "name \"ex\"\nflag \"--timeout <value>\"\nflag \"-v\" count=#true\nflag \"--force\"\narg \"[file]\"".parse().unwrap();
112        let opts = GoOptions::default();
113        assert_eq!(
114            generate(&spec, &opts),
115            generate_with_types(&spec, &opts, &BTreeMap::new()).unwrap()
116        );
117        for key in ["FlagMissing", "FlagV", "FlagForce", "CmdRoot"] {
118            assert!(
119                generate_with_types(
120                    &spec,
121                    &opts,
122                    &BTreeMap::from([(key.to_string(), ValueType::Duration)])
123                )
124                .is_err(),
125                "{key}"
126            );
127        }
128        let out = generate_with_types(
129            &spec,
130            &opts,
131            &BTreeMap::from([("FlagTimeout".to_string(), ValueType::Duration)]),
132        )
133        .unwrap();
134        assert!(out.contains("time.Duration"));
135        assert!(out.contains("argv.Duration(\"--timeout\", values[len(values)-1])"));
136        assert!(!out.contains("out.Timeout = ev.Value"));
137        assert!("typo".parse::<ValueType>().is_err());
138    }
139}