1use 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 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 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 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 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}