zero_sdk/
lib.rs

1use std::collections::HashMap;
2mod json;
3mod tests;
4
5/// Zero API client. Instantiate with a token, than call the `.fetch()` method to download secrets.
6pub struct Zero {
7    api_url: String,
8    caller_name: Option<String>,
9    pick: Vec<String>,
10    token: String,
11}
12
13/// Constructor arguments. Defines required and optional params.
14pub struct Arguments {
15    pub token: String,
16    pub pick: Option<Vec<String>>,
17    pub caller_name: Option<String>,
18}
19
20/// The main client for accessing Zero GraphQL API.
21///
22/// ### Example:
23/// ```rust
24/// use zero_sdk::{Zero, Arguments};
25///
26/// let client = Zero::new(Arguments {
27///     pick: Some(vec![String::from("my-secret")]),
28///     token: String::from("my-zero-token"),
29///     caller_name: None,
30/// })
31/// .unwrap();
32/// ```
33impl Zero {
34    /// Set the URL which will be called in fetch(). The method was added mostly for convenience of testing.
35    pub fn set_api_url(mut self, new_api_url: String) -> Self {
36        self.api_url = new_api_url;
37        return self;
38    }
39
40    // TODO Implement proper error structures with a message and a code
41    // TODO Accepts an array of secrets to fetch
42    /// Fetch the secrets assigned to the token.
43    pub fn fetch(self) -> Result<HashMap<String, HashMap<String, String>>, String> {
44        let response = if let Ok(value) = ureq::post(&self.api_url).send_json(serde_json::json!({
45            "query":
46                format!(
47                    "query {{
48                        secrets(zeroToken: \"{}\", pick: [{}]{}) {{
49                            name
50                            fields {{
51                                name value
52                            }}
53                        }}
54                    }}",
55                    &self.token,
56                    &self
57                        .pick
58                        .iter()
59                        .map(|secret| format!("\"{}\"", &secret))
60                        .collect::<Vec<String>>()
61                        .join(", "),
62
63                    // REVIEW There should be a better way for string interpolation
64                    if self.caller_name.is_some() {
65                        format!(", callerName: \"{}\"", &self.caller_name.unwrap_or_default())
66                    } else {
67                        "".to_string()
68                    },
69                )
70        })) {
71            value
72        } else {
73            return Err(String::from("Failed to fetch secrets due to network issue"));
74        };
75
76        let response_json = if let Ok(value) = response.into_json::<json::ResponseJson>() {
77            value
78        } else {
79            return Err(String::from("Server returned invalid response"));
80        };
81
82        if response_json.errors.is_some() {
83            return Err(String::from(&response_json.errors.unwrap()[0].message));
84        }
85
86        if response_json.data.is_none() {
87            return Err(String::from(
88                "Server returned invalid response (no secrets)",
89            ));
90        }
91
92        // Tranform response to the following structure:
93        // {nameOfTheSecret: {fieldOne: "fieldOneValue", fieldTwo: "fieldTwoValue"}}
94        Ok(response_json
95            .data
96            .unwrap()
97            .secrets
98            .unwrap()
99            .iter()
100            .map(|secret| {
101                (
102                    secret.name.to_owned(),
103                    HashMap::from_iter(
104                        secret
105                            .fields
106                            .iter()
107                            .map(|field| (field.name.to_owned(), field.value.to_owned())),
108                    ),
109                )
110            })
111            .collect())
112    }
113
114    /// Instantiate new Zero struct. Requires token string to be non empty, other params are optional.
115    pub fn new(arguments: Arguments) -> Result<Self, &'static str> {
116        if arguments.token == "" {
117            return Err("Zero-token is empty");
118        }
119
120        Ok(Self {
121            api_url: String::from("https://core.tryzero.com/graphql"),
122            caller_name: arguments.caller_name,
123            pick: arguments.pick.unwrap_or(vec![]),
124            token: arguments.token,
125        })
126    }
127}