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!(
39                "unknown Go field type {value:?}; expected int, int64, uint64, float64, bool, or duration"
40            )),
41        }
42    }
43}
44
45impl ValueType {
46    /// Spell the scalar or slice type in a generated struct.
47    pub(super) fn field_type(self, many: bool) -> String {
48        let scalar = match self {
49            Self::Int => "int",
50            Self::Int64 => "int64",
51            Self::Uint64 => "uint64",
52            Self::Float64 => "float64",
53            Self::Bool => "bool",
54            Self::Duration => "time.Duration",
55        };
56        if many {
57            format!("[]{scalar}")
58        } else {
59            scalar.to_string()
60        }
61    }
62
63    /// Reuse the runtime converter rather than generating a second conversion policy.
64    pub(super) fn converter(self) -> &'static str {
65        match self {
66            Self::Int => "NativeInt",
67            Self::Int64 => "Int",
68            Self::Uint64 => "Uint",
69            Self::Float64 => "Float",
70            Self::Bool => "Bool",
71            Self::Duration => "Duration",
72        }
73    }
74}
75
76/// Reject misspelled keys and entries that have no textual values to convert.
77pub(super) fn validate(
78    commands: &[Emitted],
79    types: &BTreeMap<String, ValueType>,
80) -> Result<(), String> {
81    for key in types.keys() {
82        let flag = commands
83            .iter()
84            .flat_map(|e| &e.flags)
85            .find(|(_, named)| &named.key == key);
86        if let Some((flag, _)) = flag {
87            if flag.count || flag.arg.is_none() {
88                return Err(format!("{key} is not a value-taking flag"));
89            }
90            continue;
91        }
92        if commands
93            .iter()
94            .flat_map(|e| &e.args)
95            .any(|(_, named)| &named.key == key)
96        {
97            continue;
98        }
99        return Err(format!(
100            "{key} is not a generated command-level flag or argument key; clause fields are not supported yet"
101        ));
102    }
103    Ok(())
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::Spec;
110    use crate::go::{GoOptions, generate, generate_with_types};
111
112    /// Typed generation is opt-in and invalid bindings fail before source is emitted.
113    #[test]
114    fn explicit_types_are_checked() {
115        let spec: Spec = "name \"ex\"\nflag \"--timeout <value>\"\nflag \"-v\" count=#true\nflag \"--force\"\narg \"[file]\"".parse().unwrap();
116        let opts = GoOptions::default();
117        assert_eq!(
118            generate(&spec, &opts),
119            generate_with_types(&spec, &opts, &BTreeMap::new()).unwrap()
120        );
121        for key in ["FlagMissing", "FlagV", "FlagForce", "CmdRoot"] {
122            assert!(
123                generate_with_types(
124                    &spec,
125                    &opts,
126                    &BTreeMap::from([(key.to_string(), ValueType::Duration)])
127                )
128                .is_err(),
129                "{key}"
130            );
131        }
132        let out = generate_with_types(
133            &spec,
134            &opts,
135            &BTreeMap::from([("FlagTimeout".to_string(), ValueType::Duration)]),
136        )
137        .unwrap();
138        assert!(out.contains("time.Duration"));
139        assert!(out.contains("argv.Duration(\"--timeout\", values[len(values)-1])"));
140        assert!(!out.contains("out.Timeout = ev.Value"));
141        assert!("typo".parse::<ValueType>().is_err());
142    }
143}