Skip to main content

vantage_cmd/models/
mod.rs

1//! `CmdModelFactory` — maps dotted model names (`iam.users`, `log.groups`,
2//! `log.group`, …) to YAML-backed [`Vista`]s, mirroring
3//! `vantage_aws::models::Factory`.
4//!
5//! The YAML vistas are bundled with the crate and used as the `aws-cli`
6//! example's model set. Add a model by dropping a `*.yaml` in `vistas/`
7//! and adding it to [`CmdModelFactory::yaml_for`] / [`known_names`].
8
9use vantage_table::table::Table;
10use vantage_types::EmptyEntity;
11use vantage_vista::{ReferenceKind, Vista};
12
13use crate::cmd::Cmd;
14use crate::vista::spec::CmdVistaSpec;
15
16const IAM_USERS: &str = include_str!("../../vistas/iam.users.yaml");
17const IAM_GROUPS: &str = include_str!("../../vistas/iam.groups.yaml");
18const IAM_ROLES: &str = include_str!("../../vistas/iam.roles.yaml");
19const IAM_POLICIES: &str = include_str!("../../vistas/iam.policies.yaml");
20const IAM_USER_GROUPS: &str = include_str!("../../vistas/iam.user_groups.yaml");
21const IAM_ACCESS_KEYS: &str = include_str!("../../vistas/iam.access_keys.yaml");
22const IAM_USER_POLICIES: &str = include_str!("../../vistas/iam.user_policies.yaml");
23const LOG_GROUPS: &str = include_str!("../../vistas/log.groups.yaml");
24const LOG_STREAMS: &str = include_str!("../../vistas/log.streams.yaml");
25const LOG_EVENTS: &str = include_str!("../../vistas/log.events.yaml");
26const ECS_CLUSTERS: &str = include_str!("../../vistas/ecs.clusters.yaml");
27const ECS_SERVICES: &str = include_str!("../../vistas/ecs.services.yaml");
28const ECS_TASKS: &str = include_str!("../../vistas/ecs.tasks.yaml");
29const ECS_TASK_DEFINITIONS: &str = include_str!("../../vistas/ecs.task_definitions.yaml");
30const S3_BUCKETS: &str = include_str!("../../vistas/s3.buckets.yaml");
31const S3_OBJECTS: &str = include_str!("../../vistas/s3.objects.yaml");
32const LAMBDA_FUNCTIONS: &str = include_str!("../../vistas/lambda.functions.yaml");
33const LAMBDA_ALIASES: &str = include_str!("../../vistas/lambda.aliases.yaml");
34const LAMBDA_VERSIONS: &str = include_str!("../../vistas/lambda.versions.yaml");
35
36/// Whether a lookup is naturally a list (plural name) or a single record
37/// (singular name).
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum FactoryMode {
40    List,
41    Single,
42}
43
44/// Builds vistas by name off a shared [`Cmd`].
45#[derive(Clone)]
46pub struct CmdModelFactory {
47    cmd: Cmd,
48}
49
50impl CmdModelFactory {
51    pub fn new(cmd: Cmd) -> Self {
52        Self { cmd }
53    }
54
55    /// Top-level model names exposed to a CLI.
56    pub fn known_names() -> &'static [&'static str] {
57        &[
58            "iam.user",
59            "iam.users",
60            "iam.group",
61            "iam.groups",
62            "iam.role",
63            "iam.roles",
64            "iam.policy",
65            "iam.policies",
66            "log.group",
67            "log.groups",
68            "ecs.cluster",
69            "ecs.clusters",
70            "ecs.task_definition",
71            "ecs.task_definitions",
72            "s3.bucket",
73            "s3.buckets",
74            "lambda.function",
75            "lambda.functions",
76        ]
77    }
78
79    /// The YAML for a model name (singular and plural map to the same spec).
80    /// Relation targets (`log.streams`, `log.events`) resolve here too even
81    /// though they aren't top-level `known_names`.
82    fn yaml_for(name: &str) -> Option<&'static str> {
83        Some(match name {
84            "iam.user" | "iam.users" => IAM_USERS,
85            "iam.group" | "iam.groups" => IAM_GROUPS,
86            "iam.role" | "iam.roles" => IAM_ROLES,
87            "iam.policy" | "iam.policies" => IAM_POLICIES,
88            // Relation targets (reached via `:groups` / `:access_keys` /
89            // `:policies` from iam.user); not surfaced top-level.
90            "iam.user_groups" => IAM_USER_GROUPS,
91            "iam.access_keys" => IAM_ACCESS_KEYS,
92            "iam.user_policies" => IAM_USER_POLICIES,
93            "log.group" | "log.groups" => LOG_GROUPS,
94            "log.stream" | "log.streams" => LOG_STREAMS,
95            "log.event" | "log.events" => LOG_EVENTS,
96            "ecs.cluster" | "ecs.clusters" => ECS_CLUSTERS,
97            "ecs.service" | "ecs.services" => ECS_SERVICES,
98            "ecs.task" | "ecs.tasks" => ECS_TASKS,
99            "ecs.task_definition" | "ecs.task_definitions" => ECS_TASK_DEFINITIONS,
100            "s3.bucket" | "s3.buckets" => S3_BUCKETS,
101            "s3.object" | "s3.objects" => S3_OBJECTS,
102            "lambda.function" | "lambda.functions" => LAMBDA_FUNCTIONS,
103            "lambda.alias" | "lambda.aliases" => LAMBDA_ALIASES,
104            "lambda.version" | "lambda.versions" => LAMBDA_VERSIONS,
105            _ => return None,
106        })
107    }
108
109    fn is_singular(name: &str) -> bool {
110        matches!(
111            name,
112            "iam.user"
113                | "iam.group"
114                | "iam.role"
115                | "iam.policy"
116                | "log.group"
117                | "log.stream"
118                | "log.event"
119                | "ecs.cluster"
120                | "ecs.service"
121                | "ecs.task"
122                | "ecs.task_definition"
123                | "s3.bucket"
124                | "s3.object"
125                | "lambda.function"
126                | "lambda.alias"
127                | "lambda.version"
128        )
129    }
130
131    /// Resolve a model name to a `Vista` plus its natural mode.
132    ///
133    /// The vista's references are lowered onto the wrapped `Table<Cmd>` (see
134    /// [`build_table`](Self::build_table)), so the CLI's `:relation`
135    /// traversal flows through the built-in `Table::get_ref_from_row` path.
136    pub fn for_name(&self, name: &str) -> Option<(Vista, FactoryMode)> {
137        let table = Self::build_table(self.cmd.clone(), name)?;
138        let vista = self.cmd.vista_factory().from_table(table).ok()?;
139        let mode = if Self::is_singular(name) {
140            FactoryMode::Single
141        } else {
142            FactoryMode::List
143        };
144        Some((vista, mode))
145    }
146
147    /// Build a fully-referenced `Table<Cmd>` for a model from its bundled
148    /// YAML. Columns / id come from [`CmdVistaFactory::build_columns_table`];
149    /// each YAML `references:` entry becomes a real `with_many` / `with_one`
150    /// registration whose target is resolved (lazily, on traversal) by name
151    /// through this same builder.
152    fn build_table(cmd: Cmd, name: &str) -> Option<Table<Cmd, EmptyEntity>> {
153        let yaml = Self::yaml_for(name)?;
154        let spec: CmdVistaSpec = serde_yaml_ng::from_str(yaml).ok()?;
155        let mut table = cmd.vista_factory().build_columns_table(&spec).ok()?;
156
157        for (rel_name, ref_spec) in &spec.references {
158            let target = ref_spec.table.clone();
159            let fk = ref_spec
160                .foreign_key
161                .clone()
162                .unwrap_or_else(|| rel_name.clone());
163            // The target is bundled YAML, so a resolve failure is a build-time
164            // bug, not a runtime condition — surface it loudly.
165            let build_target = move |cmd: Cmd| {
166                Self::build_table(cmd, &target)
167                    .expect("bundled vista reference target must resolve")
168            };
169            table = match ref_spec.kind {
170                ReferenceKind::HasMany => table.with_many(rel_name, &fk, build_target),
171                ReferenceKind::HasOne => table.with_one(rel_name, &fk, build_target),
172            };
173        }
174
175        Some(table)
176    }
177}