Skip to main content

vantage_aws/models/
mod.rs

1//! Ready-made tables to skip the table-name dance.
2//!
3//! CloudWatch Logs (JSON-1.1, under [`logs`]):
4//!   - [`logs::groups_table`]  — `DescribeLogGroups`
5//!   - [`logs::streams_table`] — `DescribeLogStreams`
6//!   - [`logs::events_table`]  — `FilterLogEvents`
7//!
8//! ECS (JSON-1.1, under [`ecs`]):
9//!   - [`ecs::clusters_table`]
10//!   - [`ecs::services_table`]
11//!   - [`ecs::tasks_table`]
12//!   - [`ecs::task_definitions_table`]
13//!
14//! IAM (Query, under [`iam`]):
15//!   - [`iam::users_table`]              — `ListUsers`
16//!   - [`iam::groups_table`]             — `ListGroups`
17//!   - [`iam::roles_table`]              — `ListRoles`
18//!   - [`iam::policies_table`]           — `ListPolicies`
19//!   - [`iam::access_keys_table`]        — `ListAccessKeys`  (per user)
20//!   - [`iam::instance_profiles_table`]  — `ListInstanceProfiles`
21//!
22//! S3 (REST-XML, under [`s3`]):
23//!   - [`s3::buckets_table`] — `ListBuckets`
24//!   - [`s3::objects_table`] — `ListObjectsV2` (per bucket)
25//!
26//! Lambda (REST-JSON, under [`lambda`]):
27//!   - [`lambda::functions_table`] — `ListFunctions`
28//!   - [`lambda::aliases_table`]   — `ListAliases` (per function)
29//!   - [`lambda::versions_table`]  — `ListVersionsByFunction` (per function)
30//!
31//! DynamoDB (JSON-1.0, under [`dynamodb`]):
32//!   - [`dynamodb::tables_table`] — `ListTables`
33//!
34//! ## Generic factory ([`Factory`])
35//!
36//! Wraps every table above behind dotted-string names (`iam.users`,
37//! `log.group`, `ecs.task_definitions`, …) and a single
38//! [`Factory::from_arn`] entry point. Powers the `aws-cli` example
39//! (which adapts it to `vantage_cli_util`'s `ModelFactory` trait);
40//! anything else that needs a generic, type-erased AWS table by name
41//! can reuse it without dragging in a CLI rendering crate.
42//!
43//! ```no_run
44//! # use vantage_aws::{AwsAccount, eq};
45//! # use vantage_aws::models::logs::groups_table;
46//! # async fn run() -> vantage_core::Result<()> {
47//! let aws = AwsAccount::from_default()?;
48//! let mut groups = groups_table(aws);
49//! groups.add_condition(eq("logGroupNamePrefix", "/aws/lambda/"));
50//! # Ok(()) }
51//! ```
52
53pub mod dynamodb;
54pub mod ecs;
55pub mod iam;
56pub mod lambda;
57pub mod logs;
58pub mod s3;
59
60use vantage_vista::Vista;
61
62use crate::AwsAccount;
63
64/// Whether a [`Factory`] lookup should drop into list mode (returning
65/// every matching record) or single-record mode (returning just the
66/// first match).
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum FactoryMode {
69    List,
70    Single,
71}
72
73/// Generic, type-erased model factory.
74///
75/// The factory maps dotted string names to the typed `*_table`
76/// factories above and dispatches ARN parsing across each entity's
77/// `from_arn`. Singular forms (e.g. `iam.user`) drop into
78/// [`FactoryMode::Single`]; plural forms (`iam.users`) drop into
79/// [`FactoryMode::List`].
80#[derive(Debug, Clone)]
81pub struct Factory {
82    aws: AwsAccount,
83}
84
85impl Factory {
86    /// Build a factory bound to a specific AWS account.
87    pub fn new(aws: AwsAccount) -> Self {
88        Self { aws }
89    }
90
91    /// All known model names, in registration order.
92    ///
93    /// Models whose AWS API requires a parent filter aren't exposed
94    /// top-level — listing them standalone would either error
95    /// or quietly return only the caller's slice. Reach them via
96    /// traversal from their parent:
97    ///   - `iam.user ... :access_keys`        (ListAccessKeys needs UserName)
98    ///   - `log.group ... :streams`           (DescribeLogStreams needs logGroupName)
99    ///   - `log.group ... :events`            (FilterLogEvents needs logGroupName)
100    ///   - `ecs.cluster ... :services`        (ListServices needs cluster)
101    ///   - `ecs.cluster ... :tasks`           (ListTasks needs cluster)
102    ///   - `s3.bucket ... :objects`           (ListObjectsV2 needs Bucket)
103    ///   - `lambda.function ... :aliases`     (ListAliases needs FunctionName)
104    ///   - `lambda.function ... :versions`    (ListVersionsByFunction needs FunctionName)
105    ///   - `lambda.function ... :log_group`   (CloudWatch group at /aws/lambda/<name>)
106    ///
107    /// Per-resource ARNs still work as the first argument for any of
108    /// these — see [`Factory::from_arn`].
109    pub fn known_names() -> &'static [&'static str] {
110        &[
111            "iam.user",
112            "iam.users",
113            "iam.group",
114            "iam.groups",
115            "iam.role",
116            "iam.roles",
117            "iam.policy",
118            "iam.policies",
119            "iam.instance_profile",
120            "iam.instance_profiles",
121            "log.group",
122            "log.groups",
123            "ecs.cluster",
124            "ecs.clusters",
125            "ecs.task_definition",
126            "ecs.task_definitions",
127            "s3.bucket",
128            "s3.buckets",
129            "lambda.function",
130            "lambda.functions",
131            "dynamodb.table",
132            "dynamodb.tables",
133        ]
134    }
135
136    /// Resolve a model name to a fully-constructed [`Vista`] plus its
137    /// natural mode (singular → [`FactoryMode::Single`], plural →
138    /// [`FactoryMode::List`]).
139    ///
140    /// Composite-id endpoints (`iam.access_keys`, `s3.objects`,
141    /// `lambda.aliases`, `lambda.versions`, `ecs.services`, `ecs.tasks`,
142    /// `log.streams`, `log.events`) intentionally aren't surfaced here:
143    /// the AWS list endpoint requires a parent filter, so the only
144    /// useful way to reach them is via `:relation` traversal from the
145    /// parent.
146    pub fn for_name(&self, name: &str) -> Option<(Vista, FactoryMode)> {
147        let aws = self.aws.clone();
148        let factory = aws.vista_factory();
149        let (vista, mode) = match name {
150            "iam.user" => (
151                factory.from_table(iam::users_table(aws)).ok()?,
152                FactoryMode::Single,
153            ),
154            "iam.users" => (
155                factory.from_table(iam::users_table(aws)).ok()?,
156                FactoryMode::List,
157            ),
158            "iam.group" => (
159                factory.from_table(iam::groups_table(aws)).ok()?,
160                FactoryMode::Single,
161            ),
162            "iam.groups" => (
163                factory.from_table(iam::groups_table(aws)).ok()?,
164                FactoryMode::List,
165            ),
166            "iam.role" => (
167                factory.from_table(iam::roles_table(aws)).ok()?,
168                FactoryMode::Single,
169            ),
170            "iam.roles" => (
171                factory.from_table(iam::roles_table(aws)).ok()?,
172                FactoryMode::List,
173            ),
174            "iam.policy" => (
175                factory.from_table(iam::policies_table(aws)).ok()?,
176                FactoryMode::Single,
177            ),
178            "iam.policies" => (
179                factory.from_table(iam::policies_table(aws)).ok()?,
180                FactoryMode::List,
181            ),
182            "iam.instance_profile" => (
183                factory.from_table(iam::instance_profiles_table(aws)).ok()?,
184                FactoryMode::Single,
185            ),
186            "iam.instance_profiles" => (
187                factory.from_table(iam::instance_profiles_table(aws)).ok()?,
188                FactoryMode::List,
189            ),
190            "log.group" => (
191                factory.from_table(logs::groups_table(aws)).ok()?,
192                FactoryMode::Single,
193            ),
194            "log.groups" => (
195                factory.from_table(logs::groups_table(aws)).ok()?,
196                FactoryMode::List,
197            ),
198            "ecs.cluster" => (
199                factory.from_table(ecs::clusters_table(aws)).ok()?,
200                FactoryMode::Single,
201            ),
202            "ecs.clusters" => (
203                factory.from_table(ecs::clusters_table(aws)).ok()?,
204                FactoryMode::List,
205            ),
206            "ecs.task_definition" => (
207                factory.from_table(ecs::task_definitions_table(aws)).ok()?,
208                FactoryMode::Single,
209            ),
210            "ecs.task_definitions" => (
211                factory.from_table(ecs::task_definitions_table(aws)).ok()?,
212                FactoryMode::List,
213            ),
214            "s3.bucket" => (
215                factory.from_table(s3::buckets_table(aws)).ok()?,
216                FactoryMode::Single,
217            ),
218            "s3.buckets" => (
219                factory.from_table(s3::buckets_table(aws)).ok()?,
220                FactoryMode::List,
221            ),
222            "lambda.function" => (
223                factory.from_table(lambda::functions_table(aws)).ok()?,
224                FactoryMode::Single,
225            ),
226            "lambda.functions" => (
227                factory.from_table(lambda::functions_table(aws)).ok()?,
228                FactoryMode::List,
229            ),
230            "dynamodb.table" => (
231                factory.from_table(dynamodb::tables_table(aws)).ok()?,
232                FactoryMode::Single,
233            ),
234            "dynamodb.tables" => (
235                factory.from_table(dynamodb::tables_table(aws)).ok()?,
236                FactoryMode::List,
237            ),
238            _ => return None,
239        };
240        Some((vista, mode))
241    }
242
243    /// Resolve an ARN to a pre-conditioned single-record [`Vista`].
244    /// Returns `None` if no entity recognises the ARN's resource type.
245    ///
246    /// Dispatch order: each entity's `from_arn` runs in turn — S3 object
247    /// ARNs (`arn:aws:s3:::bucket/key`) are probed before bucket ARNs
248    /// since the object form is a strict superset.
249    pub fn from_arn(&self, arn: &str) -> Option<Vista> {
250        let aws = self.aws.clone();
251        let factory = aws.vista_factory();
252        if let Some(t) = iam::user::User::from_arn(arn, aws.clone()) {
253            return factory.from_table(t).ok();
254        }
255        if let Some(t) = iam::group::Group::from_arn(arn, aws.clone()) {
256            return factory.from_table(t).ok();
257        }
258        if let Some(t) = iam::role::Role::from_arn(arn, aws.clone()) {
259            return factory.from_table(t).ok();
260        }
261        if let Some(t) = iam::policy::Policy::from_arn(arn, aws.clone()) {
262            return factory.from_table(t).ok();
263        }
264        if let Some(t) = iam::instance_profile::InstanceProfile::from_arn(arn, aws.clone()) {
265            return factory.from_table(t).ok();
266        }
267        if let Some(t) = iam::access_key::AccessKey::from_arn(arn, aws.clone()) {
268            return factory.from_table(t).ok();
269        }
270        if let Some(t) = logs::stream::LogStream::from_arn(arn, aws.clone()) {
271            return factory.from_table(t).ok();
272        }
273        if let Some(t) = logs::group::LogGroup::from_arn(arn, aws.clone()) {
274            return factory.from_table(t).ok();
275        }
276        if let Some(t) = ecs::cluster::Cluster::from_arn(arn, aws.clone()) {
277            return factory.from_table(t).ok();
278        }
279        if let Some(t) = s3::object::Object::from_arn(arn, aws.clone()) {
280            return factory.from_table(t).ok();
281        }
282        if let Some(t) = s3::bucket::Bucket::from_arn(arn, aws.clone()) {
283            return factory.from_table(t).ok();
284        }
285        if let Some(t) = lambda::function::Function::from_arn(arn, aws.clone()) {
286            return factory.from_table(t).ok();
287        }
288        if let Some(t) = dynamodb::table::DynamoDbTable::from_arn(arn, aws.clone()) {
289            return factory.from_table(t).ok();
290        }
291        None
292    }
293}