Skip to main content

az_rs/api/
invoke.rs

1use core::unreachable;
2use std::collections::HashMap;
3
4use crate::client::Client;
5
6use super::metadata::{Command, Operation, Schema};
7use anyhow::{bail, Result};
8use clap::ArgMatches;
9
10pub struct CommandInvocation {
11    command: Command,
12    matches: ArgMatches,
13}
14
15impl CommandInvocation {
16    pub fn new(command: &Command, matches: &ArgMatches) -> Self {
17        Self {
18            command: command.clone(),
19            matches: matches.clone(),
20        }
21    }
22
23    pub async fn invoke(&self, client: &Client) -> Result<String> {
24        if self.command.operations.is_empty() {
25            bail!("No operation found for command {}", self.command.name);
26        }
27        let operation = self.command.operations.first().unwrap();
28        let operation_ionvocation = OperationInvocation::new(operation, &self.matches);
29        operation_ionvocation.invoke(client).await
30    }
31}
32
33struct OperationInvocation {
34    operation: Operation,
35    matches: ArgMatches,
36}
37
38impl OperationInvocation {
39    pub fn new(operation: &Operation, matches: &ArgMatches) -> Self {
40        Self {
41            operation: operation.clone(),
42            matches: matches.clone(),
43        }
44    }
45
46    pub async fn invoke(&self, client: &crate::client::Client) -> Result<String> {
47        if self.operation.http.is_none() {
48            bail!(
49                r#"HTTP information not found for operation "{}""#,
50                self.operation
51                    .operation_id
52                    .clone()
53                    .unwrap_or("".to_string()),
54            );
55        }
56
57        let http = self.operation.http.as_ref().unwrap();
58        let mut path = http.path.clone();
59        for param in &http.request.path.params {
60            if let Some(value) = self.matches.get_one::<String>(&param.arg) {
61                path = path.replace(&format!("{{{}}}", param.name), value);
62            } else if let Some(true) = param.required {
63                bail!("missing required path parameter: {}", param.name);
64            } else {
65                unreachable!(
66                    r#"optional path parameter "{}" not supported yet!"#,
67                    param.name
68                )
69            }
70        }
71        let mut query_pairs = HashMap::new();
72        // TODO: handle query parameters (query.params)
73        for param in &http.request.query.consts {
74            query_pairs.insert(param.name.clone(), param.default.value.clone());
75        }
76        let body: Option<bytes::Bytes> = if let Some(body_meta) = &http.request.body {
77            if let Some(schema) = &body_meta.json.schema {
78                self.build_body(schema.clone())?
79                    .map(|v| bytes::Bytes::from(v.to_string()))
80            } else {
81                None
82            }
83        } else {
84            None
85        };
86        let response = client
87            .run(
88                http.request.method.into(),
89                path.as_str(),
90                &query_pairs["api-version"],
91                body,
92                None,
93            )
94            .await?;
95        for response_meta in &http.responses {
96            if let Some(status_codes) = &response_meta.status_code {
97                if status_codes.contains(&(u16::from(response.status_code) as i64)) {
98                    return Ok(String::from_utf8(response.body.to_vec())?);
99                }
100            }
101        }
102        bail!(
103            "error response: {}\n\n{}",
104            response.status_code,
105            String::from_utf8_lossy(&response.body)
106        );
107    }
108
109    fn build_body(&self, schema: Schema) -> Result<Option<serde_json::Value>> {
110        if let Some(props) = &schema.props {
111            let mut map = serde_json::Map::new();
112            for prop in props {
113                if let Some(prop_name) = &prop.name {
114                    let value = self.build_value(prop.clone())?;
115                    if let Some(value) = value {
116                        map.insert(prop_name.clone(), value);
117                    }
118                } else {
119                    bail!(r#"property lacks the "name" in the schema"#,);
120                }
121            }
122            return Ok(Some(serde_json::Value::Object(map)));
123        }
124        bail!(r#"schema lacks the "props" in the schema"#);
125    }
126
127    fn build_value(&self, schema: Schema) -> Result<Option<serde_json::Value>> {
128        match schema.type_.as_str() {
129            "object" => {
130                if let Some(arg) = &schema.arg {
131                    if let Some(value) = self.matches.get_one::<String>(arg) {
132                        Ok(Some(serde_json::from_str(value)?))
133                    } else {
134                        Ok(None)
135                    }
136                } else if let Some(props) = &schema.props {
137                    let mut map = serde_json::Map::new();
138                    for prop in props {
139                        if let Some(prop_name) = &prop.name {
140                            let value = self.build_value(prop.clone())?;
141                            if let Some(value) = value {
142                                map.insert(prop_name.clone(), value);
143                            }
144                        } else {
145                            bail!(r#"property lacks the "name" in the schema"#,);
146                        }
147                    }
148                    if map.is_empty() {
149                        Ok(None)
150                    } else {
151                        Ok(Some(serde_json::Value::Object(map)))
152                    }
153                } else {
154                    bail!(r#"object schema lacks both the "arg" and "props" in the schema"#);
155                }
156            }
157            s if s.starts_with("array") || s == "string" => {
158                if let Some(arg) = &schema.arg {
159                    if let Some(value) = self.matches.get_one::<String>(arg) {
160                        Ok(Some(serde_json::from_str(value)?))
161                    } else {
162                        Ok(None)
163                    }
164                } else {
165                    bail!(
166                        r#"schema "{}" lacks the "arg" in the schema"#,
167                        schema.name.unwrap_or("".to_string())
168                    );
169                }
170            }
171            s if s.starts_with("integer") => {
172                if let Some(arg) = &schema.arg {
173                    if let Some(value) = self.matches.get_one::<i32>(arg) {
174                        Ok(Some((*value).into()))
175                    } else {
176                        Ok(None)
177                    }
178                } else {
179                    bail!(
180                        r#"schema "{}" lacks the "arg" in the schema"#,
181                        schema.name.unwrap_or("".to_string())
182                    );
183                }
184            }
185            "boolean" => {
186                if let Some(arg) = &schema.arg {
187                    if let Some(value) = self.matches.get_one::<bool>(arg) {
188                        Ok(Some((*value).into()))
189                    } else {
190                        Ok(None)
191                    }
192                } else {
193                    bail!(
194                        r#"schema "{}" lacks the "arg" in the schema"#,
195                        schema.name.unwrap_or("".to_string())
196                    );
197                }
198            }
199            // TODO: We shall handle float and other potential types in the metadata.
200            //       Then fail the other cases.
201            _ => {
202                // We suppose any other type as a json value first, if failed, try to parse it as a string
203                if let Some(arg) = &schema.arg {
204                    if let Some(value) = self.matches.get_one::<String>(arg) {
205                        match serde_json::from_str(value) {
206                            Ok(v) => Ok(Some(v)),
207                            Err(_) => Ok(Some(serde_json::Value::String(value.clone()))),
208                        }
209                    } else {
210                        Ok(None)
211                    }
212                } else {
213                    bail!(
214                        r#"schema "{}" lacks the "arg" in the schema"#,
215                        schema.name.unwrap_or("".to_string())
216                    );
217                }
218            }
219        }
220    }
221}