Skip to main content

az_rs/api/
metadata.rs

1// TODO: This is not a comprehensive definition for the API metadata, just for PoC.
2use anyhow::{anyhow, bail, Result};
3use serde::Deserialize;
4
5use crate::arg::CliInput;
6
7#[cfg_attr(test, derive(serde::Serialize))]
8#[derive(Debug, Clone, Deserialize)]
9pub struct Metadata {
10    pub plane: Plane,
11    #[serde(rename = "commandGroups")]
12    pub command_groups: Vec<CommandGroup>,
13    pub resources: Vec<Resource>,
14}
15
16#[cfg_attr(test, derive(serde::Serialize))]
17#[derive(Debug, Copy, Clone, Deserialize)]
18pub enum Plane {
19    #[serde(rename = "mgmt-plane")]
20    Mgmt,
21    #[serde(rename = "data-plane")]
22    Data,
23}
24
25#[cfg_attr(test, derive(serde::Serialize))]
26#[derive(Debug, Copy, Clone, Deserialize)]
27#[serde(rename_all = "lowercase")]
28pub enum Method {
29    Head,
30    Get,
31    Put,
32    Patch,
33    Post,
34    Delete,
35}
36
37impl From<Method> for azure_core::http::Method {
38    fn from(method: Method) -> Self {
39        match method {
40            Method::Head => azure_core::http::Method::Head,
41            Method::Get => azure_core::http::Method::Get,
42            Method::Put => azure_core::http::Method::Put,
43            Method::Patch => azure_core::http::Method::Patch,
44            Method::Post => azure_core::http::Method::Post,
45            Method::Delete => azure_core::http::Method::Delete,
46        }
47    }
48}
49
50#[cfg_attr(test, derive(serde::Serialize))]
51#[derive(Debug, Clone, Deserialize, Default)]
52pub struct CommandGroup {
53    pub name: String,
54    pub commands: Vec<Command>,
55    #[serde(rename = "commandGroups")]
56    pub command_groups: Option<Vec<CommandGroup>>,
57}
58
59#[cfg_attr(test, derive(serde::Serialize))]
60#[derive(Debug, Clone, Deserialize, Default)]
61pub struct Command {
62    pub resources: Vec<Resource>,
63    pub name: String,
64    pub version: String,
65    #[serde(rename = "argGroups")]
66    pub arg_groups: Vec<ArgGroup>,
67    pub operations: Vec<Operation>,
68    pub outputs: Option<Vec<Output>>,
69    pub confirmation: Option<String>,
70}
71
72#[cfg_attr(test, derive(serde::Serialize))]
73#[derive(Debug, Clone, Deserialize)]
74pub struct Resource {
75    pub id: String,
76    pub version: String,
77    pub swagger: String,
78}
79
80#[cfg_attr(test, derive(serde::Serialize))]
81#[derive(Debug, Clone, Deserialize)]
82pub struct ArgGroup {
83    pub name: String,
84    pub args: Vec<Arg>,
85}
86
87#[cfg_attr(test, derive(serde::Serialize))]
88#[derive(Debug, Clone, Deserialize)]
89pub struct Arg {
90    #[serde(rename = "type")]
91    pub type_: String,
92    pub var: String,
93    pub options: Vec<String>,
94    pub group: Option<String>,
95    pub help: Option<Help>,
96    pub required: Option<bool>,
97    #[serde(rename = "idPart")]
98    pub id_part: Option<String>,
99    #[serde(rename = "additionalProps")]
100    pub additional_props: Option<AdditionalPropSchema>,
101}
102
103#[cfg_attr(test, derive(serde::Serialize))]
104#[derive(Debug, Clone, Deserialize)]
105pub struct Help {
106    pub short: String,
107}
108
109#[cfg_attr(test, derive(serde::Serialize))]
110#[derive(Debug, Clone, Deserialize)]
111pub struct Operation {
112    #[serde(rename = "operationId")]
113    pub operation_id: Option<String>,
114    pub http: Option<Http>,
115}
116
117#[cfg_attr(test, derive(serde::Serialize))]
118#[derive(Debug, Clone, Deserialize)]
119pub struct Http {
120    pub path: String,
121    pub request: Request,
122    pub responses: Vec<Response>,
123}
124
125#[cfg_attr(test, derive(serde::Serialize))]
126#[derive(Debug, Clone, Deserialize)]
127pub struct Request {
128    pub method: Method,
129    pub path: RequestPath,
130    pub query: RequestQuery,
131    pub body: Option<Body>,
132}
133
134#[cfg_attr(test, derive(serde::Serialize))]
135#[derive(Debug, Clone, Deserialize)]
136pub struct RequestPath {
137    pub params: Vec<RequestPathParam>,
138}
139
140#[cfg_attr(test, derive(serde::Serialize))]
141#[derive(Debug, Clone, Deserialize)]
142pub struct RequestPathParam {
143    #[serde(rename = "type")]
144    pub type_: String,
145    pub name: String,
146    pub arg: String,
147    pub required: Option<bool>,
148    pub format: Option<RequestFormat>,
149}
150
151#[cfg_attr(test, derive(serde::Serialize))]
152#[derive(Debug, Clone, Deserialize)]
153pub struct RequestFormat {
154    pub pattern: String,
155    #[serde(rename = "maxLength")]
156    pub max_length: i64,
157    #[serde(rename = "minLength")]
158    pub min_length: i64,
159}
160
161#[cfg_attr(test, derive(serde::Serialize))]
162#[derive(Debug, Clone, Deserialize)]
163pub struct ResponseFormat {
164    pub template: Option<String>,
165}
166
167#[cfg_attr(test, derive(serde::Serialize))]
168#[derive(Debug, Clone, Deserialize)]
169pub struct RequestQuery {
170    pub consts: Vec<RequestQueryConst>,
171}
172
173#[cfg_attr(test, derive(serde::Serialize))]
174#[derive(Debug, Clone, Deserialize)]
175pub struct RequestQueryConst {
176    pub name: String,
177    #[serde(rename = "type")]
178    pub type_: String,
179    pub required: Option<bool>,
180    #[serde(rename = "readOnly")]
181    pub read_only: Option<bool>,
182    #[serde(rename = "const")]
183    pub const_: bool,
184    pub default: DefaultValue,
185}
186
187#[cfg_attr(test, derive(serde::Serialize))]
188#[derive(Debug, Clone, Deserialize)]
189pub struct DefaultValue {
190    pub value: String,
191}
192
193#[cfg_attr(test, derive(serde::Serialize))]
194#[derive(Debug, Clone, Deserialize)]
195pub struct Body {
196    pub json: BodyJSON,
197}
198
199#[cfg_attr(test, derive(serde::Serialize))]
200#[derive(Debug, Clone, Deserialize)]
201pub struct BodyJSON {
202    pub schema: Option<Schema>,
203    // Only applies for response body
204    pub var: Option<String>,
205    #[serde(rename = "ref")]
206    pub ref_: Option<String>,
207}
208
209#[cfg_attr(test, derive(serde::Serialize))]
210#[derive(Debug, Clone, Deserialize)]
211pub struct Response {
212    #[serde(rename = "statusCode")]
213    pub status_code: Option<Vec<i64>>,
214    pub body: Option<Body>,
215    #[serde(rename = "isError")]
216    pub is_error: Option<bool>,
217}
218
219#[cfg_attr(test, derive(serde::Serialize))]
220#[derive(Debug, Clone, Deserialize)]
221pub struct Output {
222    #[serde(rename = "type")]
223    pub type_: String,
224    #[serde(rename = "ref")]
225    pub ref_: String,
226    #[serde(rename = "clientFlatten")]
227    pub client_flatten: Option<bool>,
228}
229
230#[cfg_attr(test, derive(serde::Serialize))]
231#[derive(Debug, Clone, Deserialize)]
232pub struct Schema {
233    #[serde(rename = "type")]
234    pub type_: String,
235    pub name: Option<String>,
236    pub required: Option<bool>,
237    pub arg: Option<String>,
238    #[serde(rename = "readOnly")]
239    pub read_only: Option<bool>,
240    pub props: Option<Vec<Schema>>,
241    pub item: Option<Box<Schema>>,
242    pub format: Option<ResponseFormat>,
243    #[serde(rename = "clientFlatten")]
244    pub client_flatten: Option<bool>,
245    #[serde(rename = "additionalProps")]
246    pub additional_props: Option<AdditionalPropSchema>,
247}
248
249#[cfg_attr(test, derive(serde::Serialize))]
250#[derive(Debug, Clone, Deserialize)]
251pub struct AdditionalPropSchema {
252    pub item: AdditionalPropItemSchema,
253}
254
255#[cfg_attr(test, derive(serde::Serialize))]
256#[derive(Debug, Clone, Deserialize)]
257pub struct AdditionalPropItemSchema {
258    #[serde(rename = "type")]
259    pub type_: String,
260}
261
262impl Metadata {
263    pub fn resolve_command(&self, input: &CliInput) -> Result<Command> {
264        if input.is_empty() {
265            bail!("empty CLI input");
266        }
267
268        let args = input.pos_args();
269        let mut args = args.iter();
270        // Iterate over the first rp arg
271        args.next();
272
273        let mut cg = CommandGroup {
274            command_groups: Some(self.command_groups.clone()),
275            ..CommandGroup::default()
276        };
277
278        while let Some(arg) = args.next() {
279            if let Some(v) = cg
280                .command_groups
281                .and_then(|cgs| cgs.iter().find(|cg| cg.name.as_str() == *arg).cloned())
282            {
283                cg = v;
284            } else if let Some(v) = cg
285                .commands
286                .iter()
287                .find(|c| c.name.as_str() == *arg)
288                .cloned()
289            {
290                // Command must be the last positional argument
291                if let Some(arg) = args.next() {
292                    return Err(anyhow!("unknown argument {}", arg));
293                } else {
294                    return Ok(v);
295                }
296            } else {
297                return Err(anyhow!("unknown argument {}", arg));
298            }
299        }
300
301        return Err(anyhow!("this isn't a command"));
302    }
303}
304
305#[cfg(test)]
306mod test {
307    use super::*;
308    use pretty_assertions::assert_eq;
309    use serde::Serialize;
310    use serde_json::Value;
311    use std::error::Error;
312
313    // This is used to skip optional fields when they are None.
314    fn to_clean_json<T: Serialize>(value: &T) -> Value {
315        fn strip_nulls(value: Value) -> Value {
316            match value {
317                Value::Object(map) => {
318                    let cleaned = map
319                        .into_iter()
320                        .filter_map(|(k, v)| {
321                            let v = strip_nulls(v);
322                            if v.is_null() {
323                                None
324                            } else {
325                                Some((k, v))
326                            }
327                        })
328                        .collect();
329                    Value::Object(cleaned)
330                }
331                Value::Array(arr) => {
332                    let cleaned = arr
333                        .into_iter()
334                        .map(strip_nulls)
335                        .filter(|v| !v.is_null())
336                        .collect();
337                    Value::Array(cleaned)
338                }
339                other => other,
340            }
341        }
342
343        let raw = serde_json::to_value(value).expect("Serialization failed");
344        strip_nulls(raw)
345    }
346
347    #[test]
348    fn deserialize() -> Result<(), Box<dyn Error>> {
349        let input = r#"
350{
351  "plane": "mgmt-plane",
352  "resources": [
353    {
354      "id": "/subscriptions/{}/resourcegroups/{}",
355      "version": "2024-11-01",
356      "swagger": "mgmt-plane/resources/ResourceProviders/Microsoft.Resources/Paths/L3N1YnNjcmlwdGlvbnMve3N1YnNjcmlwdGlvbklkfS9yZXNvdXJjZWdyb3Vwcy97cmVzb3VyY2VHcm91cE5hbWV9/V/MjAyNC0xMS0wMQ=="
357    }
358  ],
359  "commandGroups": [
360    {
361      "name": "group",
362      "commands": [
363        {
364          "name": "show",
365          "version": "2024-11-01",
366          "resources": [
367            {
368              "id": "/subscriptions/{}/resourcegroups/{}",
369              "version": "2024-11-01",
370              "swagger": "mgmt-plane/resources/ResourceProviders/Microsoft.Resources/Paths/L3N1YnNjcmlwdGlvbnMve3N1YnNjcmlwdGlvbklkfS9yZXNvdXJjZWdyb3Vwcy97cmVzb3VyY2VHcm91cE5hbWV9/V/MjAyNC0xMS0wMQ=="
371            }
372          ],
373          "argGroups": [
374            {
375              "name": "",
376              "args": [
377                {
378                  "type": "ResourceGroupName",
379                  "var": "$Path.resourceGroupName",
380                  "options": [
381                    "g",
382                    "resource-group"
383                  ],
384                  "required": true,
385                  "idPart": "resource_group"
386                },
387                {
388                  "type": "SubscriptionId",
389                  "var": "$Path.subscriptionId",
390                  "options": [
391                    "subscription"
392                  ],
393                  "required": true,
394                  "idPart": "subscription"
395                }
396              ]
397            }
398          ],
399          "operations": [
400            {
401              "operationId": "ResourceGroups_Get",
402              "http": {
403                "path": "/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}",
404                "request": {
405                  "method": "get",
406                  "path": {
407                    "params": [
408                      {
409                        "type": "string",
410                        "name": "resourceGroupName",
411                        "arg": "$Path.resourceGroupName",
412                        "required": true,
413                        "format": {
414                          "pattern": "^[-\\w\\._\\(\\)]+$",
415                          "maxLength": 90,
416                          "minLength": 1
417                        }
418                      },
419                      {
420                        "type": "string",
421                        "name": "subscriptionId",
422                        "arg": "$Path.subscriptionId",
423                        "required": true
424                      }
425                    ]
426                  },
427                  "query": {
428                    "consts": [
429                      {
430                        "readOnly": true,
431                        "const": true,
432                        "default": {
433                          "value": "2024-11-01"
434                        },
435                        "type": "string",
436                        "name": "api-version",
437                        "required": true
438                      }
439                    ]
440                  }
441                },
442                "responses": [
443                  {
444                    "statusCode": [
445                      200
446                    ],
447                    "body": {
448                      "json": {
449                        "var": "$Instance",
450                        "schema": {
451                          "type": "object",
452                          "props": [
453                            {
454                              "readOnly": true,
455                              "type": "ResourceId",
456                              "name": "id",
457                              "format": {
458                                "template": "/subscriptions/{}/resourcegroups/{}"
459                              }
460                            },
461                            {
462                              "type": "ResourceLocation",
463                              "name": "location",
464                              "required": true
465                            },
466                            {
467                              "type": "string",
468                              "name": "managedBy"
469                            },
470                            {
471                              "readOnly": true,
472                              "type": "string",
473                              "name": "name"
474                            },
475                            {
476                              "type": "object",
477                              "name": "properties",
478                              "props": [
479                                {
480                                  "readOnly": true,
481                                  "type": "string",
482                                  "name": "provisioningState"
483                                }
484                              ]
485                            },
486                            {
487                              "type": "object",
488                              "name": "tags",
489                              "additionalProps": {
490                                "item": {
491                                  "type": "string"
492                                }
493                              }
494                            },
495                            {
496                              "readOnly": true,
497                              "type": "string",
498                              "name": "type"
499                            }
500                          ]
501                        }
502                      }
503                    }
504                  },
505                  {
506                    "isError": true,
507                    "body": {
508                      "json": {
509                        "schema": {
510                          "type": "@MgmtErrorFormat"
511                        }
512                      }
513                    }
514                  }
515                ]
516              }
517            }
518          ],
519          "outputs": [
520            {
521              "type": "object",
522              "ref": "$Instance",
523              "clientFlatten": true
524            }
525          ]
526        },
527        {
528          "name": "create",
529          "version": "2024-11-01",
530          "resources": [
531            {
532              "id": "/subscriptions/{}/resourcegroups/{}",
533              "version": "2024-11-01",
534              "swagger": "mgmt-plane/resources/ResourceProviders/Microsoft.Resources/Paths/L3N1YnNjcmlwdGlvbnMve3N1YnNjcmlwdGlvbklkfS9yZXNvdXJjZWdyb3Vwcy97cmVzb3VyY2VHcm91cE5hbWV9/V/MjAyNC0xMS0wMQ=="
535            }
536          ],
537          "argGroups": [
538            {
539              "name": "",
540              "args": [
541                {
542                  "type": "ResourceGroupName",
543                  "var": "$Path.resourceGroupName",
544                  "options": [
545                    "g",
546                    "resource-group"
547                  ],
548                  "required": true,
549                  "idPart": "resource_group"
550                },
551                {
552                  "type": "SubscriptionId",
553                  "var": "$Path.subscriptionId",
554                  "options": [
555                    "subscription"
556                  ],
557                  "required": true,
558                  "idPart": "subscription"
559                }
560              ]
561            },
562            {
563              "name": "Parameters",
564              "args": [
565                {
566                  "type": "ResourceLocation",
567                  "var": "$parameters.location",
568                  "options": [
569                    "l",
570                    "location"
571                  ],
572                  "required": true,
573                  "group": "Parameters",
574                  "help": {
575                    "short": "The location of the resource group. It cannot be changed after the resource group has been created. It must be one of the supported Azure locations."
576                  }
577                },
578                {
579                  "type": "string",
580                  "var": "$parameters.managedBy",
581                  "options": [
582                    "managed-by"
583                  ],
584                  "group": "Parameters",
585                  "help": {
586                    "short": "The ID of the resource that manages this resource group."
587                  }
588                },
589                {
590                  "type": "object",
591                  "var": "$parameters.tags",
592                  "options": [
593                    "tags"
594                  ],
595                  "group": "Parameters",
596                  "help": {
597                    "short": "The tags attached to the resource group."
598                  },
599                  "additionalProps": {
600                    "item": {
601                      "type": "string"
602                    }
603                  }
604                }
605              ]
606            }
607          ],
608          "operations": [
609            {
610              "operationId": "ResourceGroups_CreateOrUpdate",
611              "http": {
612                "path": "/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}",
613                "request": {
614                  "method": "put",
615                  "path": {
616                    "params": [
617                      {
618                        "type": "string",
619                        "name": "resourceGroupName",
620                        "arg": "$Path.resourceGroupName",
621                        "required": true,
622                        "format": {
623                          "pattern": "^[-\\w\\._\\(\\)]+$",
624                          "maxLength": 90,
625                          "minLength": 1
626                        }
627                      },
628                      {
629                        "type": "string",
630                        "name": "subscriptionId",
631                        "arg": "$Path.subscriptionId",
632                        "required": true
633                      }
634                    ]
635                  },
636                  "query": {
637                    "consts": [
638                      {
639                        "readOnly": true,
640                        "const": true,
641                        "default": {
642                          "value": "2024-11-01"
643                        },
644                        "type": "string",
645                        "name": "api-version",
646                        "required": true
647                      }
648                    ]
649                  },
650                  "body": {
651                    "json": {
652                      "schema": {
653                        "type": "object",
654                        "name": "parameters",
655                        "required": true,
656                        "props": [
657                          {
658                            "type": "ResourceLocation",
659                            "name": "location",
660                            "arg": "$parameters.location",
661                            "required": true
662                          },
663                          {
664                            "type": "string",
665                            "name": "managedBy",
666                            "arg": "$parameters.managedBy"
667                          },
668                          {
669                            "type": "object",
670                            "name": "tags",
671                            "arg": "$parameters.tags",
672                            "additionalProps": {
673                              "item": {
674                                "type": "string"
675                              }
676                            }
677                          }
678                        ],
679                        "clientFlatten": true
680                      }
681                    }
682                  }
683                },
684                "responses": [
685                  {
686                    "statusCode": [
687                      200,
688                      201
689                    ],
690                    "body": {
691                      "json": {
692                        "var": "$Instance",
693                        "schema": {
694                          "type": "object",
695                          "props": [
696                            {
697                              "readOnly": true,
698                              "type": "ResourceId",
699                              "name": "id",
700                              "format": {
701                                "template": "/subscriptions/{}/resourcegroups/{}"
702                              }
703                            },
704                            {
705                              "type": "ResourceLocation",
706                              "name": "location",
707                              "required": true
708                            },
709                            {
710                              "type": "string",
711                              "name": "managedBy"
712                            },
713                            {
714                              "readOnly": true,
715                              "type": "string",
716                              "name": "name"
717                            },
718                            {
719                              "type": "object",
720                              "name": "properties",
721                              "props": [
722                                {
723                                  "readOnly": true,
724                                  "type": "string",
725                                  "name": "provisioningState"
726                                }
727                              ]
728                            },
729                            {
730                              "type": "object",
731                              "name": "tags",
732                              "additionalProps": {
733                                "item": {
734                                  "type": "string"
735                                }
736                              }
737                            },
738                            {
739                              "readOnly": true,
740                              "type": "string",
741                              "name": "type"
742                            }
743                          ]
744                        }
745                      }
746                    }
747                  },
748                  {
749                    "isError": true,
750                    "body": {
751                      "json": {
752                        "schema": {
753                          "type": "@MgmtErrorFormat"
754                        }
755                      }
756                    }
757                  }
758                ]
759              }
760            }
761          ],
762          "outputs": [
763            {
764              "type": "object",
765              "ref": "$Instance",
766              "clientFlatten": true
767            }
768          ],
769          "confirmation": ""
770        }
771      ]
772    }
773  ]
774}
775            "#;
776        let metadata: Metadata = serde_json::from_str(input)?;
777        let input_json: Value = serde_json::from_str(input)?;
778        let output_json: Value = to_clean_json(&metadata);
779        assert_eq!(input_json, output_json);
780        Ok(())
781    }
782}