Skip to main content

fakecloud_codeartifact/
service.rs

1//! AWS CodeArtifact restJson1 dispatch + operation handlers.
2//!
3//! Requests are routed to an operation by HTTP method + `@http` URI path
4//! (there are no path labels -- every input parameter is a query-string value,
5//! a JSON body field, an `@httpPayload` blob, or a header). Query parameters
6//! are read from the raw query string so repeated multi-value keys (for example
7//! `?versions=a&versions=b` do not appear here, but list filters that could)
8//! survive intact, sidestepping the collapsing `HashMap` used elsewhere.
9//! Everything is real, persisted, account-partitioned CRUD.
10
11use async_trait::async_trait;
12use http::{Method, StatusCode};
13use std::sync::Arc;
14use tokio::sync::Mutex as AsyncMutex;
15
16use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
17use fakecloud_persistence::SnapshotStore;
18
19use crate::state::SharedCodeArtifactState;
20
21pub const CODEARTIFACT_ACTIONS: &[&str] = &[
22    "AssociateExternalConnection",
23    "CopyPackageVersions",
24    "CreateDomain",
25    "CreatePackageGroup",
26    "CreateRepository",
27    "DeleteDomain",
28    "DeleteDomainPermissionsPolicy",
29    "DeletePackage",
30    "DeletePackageGroup",
31    "DeletePackageVersions",
32    "DeleteRepository",
33    "DeleteRepositoryPermissionsPolicy",
34    "DescribeDomain",
35    "DescribePackage",
36    "DescribePackageGroup",
37    "DescribePackageVersion",
38    "DescribeRepository",
39    "DisassociateExternalConnection",
40    "DisposePackageVersions",
41    "GetAssociatedPackageGroup",
42    "GetAuthorizationToken",
43    "GetDomainPermissionsPolicy",
44    "GetPackageVersionAsset",
45    "GetPackageVersionReadme",
46    "GetRepositoryEndpoint",
47    "GetRepositoryPermissionsPolicy",
48    "ListAllowedRepositoriesForGroup",
49    "ListAssociatedPackages",
50    "ListDomains",
51    "ListPackageGroups",
52    "ListPackageVersionAssets",
53    "ListPackageVersionDependencies",
54    "ListPackageVersions",
55    "ListPackages",
56    "ListRepositories",
57    "ListRepositoriesInDomain",
58    "ListSubPackageGroups",
59    "ListTagsForResource",
60    "PublishPackageVersion",
61    "PutDomainPermissionsPolicy",
62    "PutPackageOriginConfiguration",
63    "PutRepositoryPermissionsPolicy",
64    "TagResource",
65    "UntagResource",
66    "UpdatePackageGroup",
67    "UpdatePackageGroupOriginConfiguration",
68    "UpdatePackageVersionsStatus",
69    "UpdateRepository",
70];
71
72/// Operations that mutate persisted state on success (so a snapshot is taken).
73const MUTATING: &[&str] = &[
74    "AssociateExternalConnection",
75    "CopyPackageVersions",
76    "CreateDomain",
77    "CreatePackageGroup",
78    "CreateRepository",
79    "DeleteDomain",
80    "DeleteDomainPermissionsPolicy",
81    "DeletePackage",
82    "DeletePackageGroup",
83    "DeletePackageVersions",
84    "DeleteRepository",
85    "DeleteRepositoryPermissionsPolicy",
86    "DisassociateExternalConnection",
87    "DisposePackageVersions",
88    "PublishPackageVersion",
89    "PutDomainPermissionsPolicy",
90    "PutPackageOriginConfiguration",
91    "PutRepositoryPermissionsPolicy",
92    "TagResource",
93    "UntagResource",
94    "UpdatePackageGroup",
95    "UpdatePackageGroupOriginConfiguration",
96    "UpdatePackageVersionsStatus",
97    "UpdateRepository",
98];
99
100pub struct CodeArtifactService {
101    state: SharedCodeArtifactState,
102    snapshot_store: Option<Arc<dyn SnapshotStore>>,
103    snapshot_lock: Arc<AsyncMutex<()>>,
104}
105
106impl CodeArtifactService {
107    pub fn new(state: SharedCodeArtifactState) -> Self {
108        Self {
109            state,
110            snapshot_store: None,
111            snapshot_lock: Arc::new(AsyncMutex::new(())),
112        }
113    }
114
115    pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
116        self.snapshot_store = Some(store);
117        self
118    }
119
120    async fn save(&self) {
121        crate::persistence::save_snapshot(
122            &self.state,
123            self.snapshot_store.clone(),
124            &self.snapshot_lock,
125        )
126        .await;
127    }
128
129    /// Persist hook for the CloudFormation provisioner; `None` in memory mode.
130    pub fn snapshot_hook(&self) -> Option<fakecloud_persistence::SnapshotHook> {
131        let store = self.snapshot_store.clone()?;
132        let state = self.state.clone();
133        let lock = self.snapshot_lock.clone();
134        Some(Arc::new(move || {
135            let state = state.clone();
136            let store = store.clone();
137            let lock = lock.clone();
138            Box::pin(async move {
139                crate::persistence::save_snapshot(&state, Some(store), &lock).await;
140            })
141        }))
142    }
143
144    /// Route a request to an operation name by HTTP method + `@http` URI path.
145    /// Returns `None` when no route matches (a 404 routing miss).
146    fn resolve_action(req: &AwsRequest) -> Option<&'static str> {
147        let raw = req.raw_path.split('?').next().unwrap_or(&req.raw_path);
148        let trimmed = raw.strip_prefix('/').unwrap_or(raw);
149        // Drop a single trailing slash so a trailing-slash variant (`/v1/domain/`)
150        // routes like its canonical form instead of yielding an empty final
151        // segment that matches nothing. No CodeArtifact route relies on a trailing
152        // empty segment.
153        let trimmed = trimmed.strip_suffix('/').unwrap_or(trimmed);
154        let segs: Vec<&str> = if trimmed.is_empty() {
155            Vec::new()
156        } else {
157            trimmed.split('/').collect()
158        };
159        let m = &req.method;
160        let post = m == Method::POST;
161        let get = m == Method::GET;
162        let del = m == Method::DELETE;
163        let put = m == Method::PUT;
164        let action = match segs.as_slice() {
165            ["v1", "domain"] if post => "CreateDomain",
166            ["v1", "domain"] if get => "DescribeDomain",
167            ["v1", "domain"] if del => "DeleteDomain",
168            ["v1", "domains"] if post => "ListDomains",
169            ["v1", "domain", "permissions", "policy"] if put => "PutDomainPermissionsPolicy",
170            ["v1", "domain", "permissions", "policy"] if get => "GetDomainPermissionsPolicy",
171            ["v1", "domain", "permissions", "policy"] if del => "DeleteDomainPermissionsPolicy",
172            ["v1", "domain", "repositories"] if post => "ListRepositoriesInDomain",
173            ["v1", "repository"] if post => "CreateRepository",
174            ["v1", "repository"] if get => "DescribeRepository",
175            ["v1", "repository"] if del => "DeleteRepository",
176            ["v1", "repository"] if put => "UpdateRepository",
177            ["v1", "repositories"] if post => "ListRepositories",
178            ["v1", "repository", "external-connection"] if post => "AssociateExternalConnection",
179            ["v1", "repository", "external-connection"] if del => "DisassociateExternalConnection",
180            ["v1", "repository", "permissions", "policy"] if get => {
181                "GetRepositoryPermissionsPolicy"
182            }
183            ["v1", "repository", "permissions", "policy"] if put => {
184                "PutRepositoryPermissionsPolicy"
185            }
186            // The model spells the DELETE path `policies` (plural).
187            ["v1", "repository", "permissions", "policies"] if del => {
188                "DeleteRepositoryPermissionsPolicy"
189            }
190            ["v1", "repository", "endpoint"] if get => "GetRepositoryEndpoint",
191            ["v1", "package"] if get => "DescribePackage",
192            ["v1", "package"] if del => "DeletePackage",
193            ["v1", "package"] if post => "PutPackageOriginConfiguration",
194            ["v1", "package", "version"] if get => "DescribePackageVersion",
195            ["v1", "package", "version", "asset"] if get => "GetPackageVersionAsset",
196            ["v1", "package", "version", "readme"] if get => "GetPackageVersionReadme",
197            ["v1", "package", "version", "publish"] if post => "PublishPackageVersion",
198            ["v1", "package", "version", "assets"] if post => "ListPackageVersionAssets",
199            ["v1", "package", "version", "dependencies"] if post => {
200                "ListPackageVersionDependencies"
201            }
202            ["v1", "package", "versions"] if post => "ListPackageVersions",
203            ["v1", "package", "versions", "copy"] if post => "CopyPackageVersions",
204            ["v1", "package", "versions", "delete"] if post => "DeletePackageVersions",
205            ["v1", "package", "versions", "dispose"] if post => "DisposePackageVersions",
206            ["v1", "package", "versions", "update_status"] if post => "UpdatePackageVersionsStatus",
207            ["v1", "packages"] if post => "ListPackages",
208            ["v1", "package-group"] if post => "CreatePackageGroup",
209            ["v1", "package-group"] if get => "DescribePackageGroup",
210            ["v1", "package-group"] if del => "DeletePackageGroup",
211            ["v1", "package-group"] if put => "UpdatePackageGroup",
212            ["v1", "package-groups"] if post => "ListPackageGroups",
213            ["v1", "package-groups", "sub-groups"] if post => "ListSubPackageGroups",
214            ["v1", "package-group-allowed-repositories"] if get => {
215                "ListAllowedRepositoriesForGroup"
216            }
217            ["v1", "package-group-origin-configuration"] if put => {
218                "UpdatePackageGroupOriginConfiguration"
219            }
220            ["v1", "get-associated-package-group"] if get => "GetAssociatedPackageGroup",
221            ["v1", "list-associated-packages"] if get => "ListAssociatedPackages",
222            ["v1", "authorization-token"] if post => "GetAuthorizationToken",
223            ["v1", "tags"] if post => "ListTagsForResource",
224            ["v1", "tag"] if post => "TagResource",
225            ["v1", "untag"] if post => "UntagResource",
226            _ => return None,
227        };
228        Some(action)
229    }
230}
231
232#[async_trait]
233impl AwsService for CodeArtifactService {
234    fn service_name(&self) -> &str {
235        "codeartifact"
236    }
237
238    async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
239        let Some(action) = Self::resolve_action(&req) else {
240            return Err(AwsServiceError::aws_error(
241                StatusCode::NOT_FOUND,
242                "UnknownOperationException",
243                format!("Unknown operation: {} {}", req.method, req.raw_path),
244            ));
245        };
246        let result = self.dispatch(action, &req);
247        if MUTATING.contains(&action)
248            && matches!(result.as_ref(), Ok(resp) if resp.status.is_success())
249        {
250            self.save().await;
251        }
252        result
253    }
254
255    fn supported_actions(&self) -> &[&str] {
256        CODEARTIFACT_ACTIONS
257    }
258}
259
260impl CodeArtifactService {
261    #[allow(clippy::too_many_lines)]
262    fn dispatch(&self, action: &str, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
263        match action {
264            // domains
265            "CreateDomain" => self.create_domain(req),
266            "DescribeDomain" => self.describe_domain(req),
267            "DeleteDomain" => self.delete_domain(req),
268            "ListDomains" => self.list_domains(req),
269            "PutDomainPermissionsPolicy" => self.put_domain_policy(req),
270            "GetDomainPermissionsPolicy" => self.get_domain_policy(req),
271            "DeleteDomainPermissionsPolicy" => self.delete_domain_policy(req),
272            // repositories
273            "CreateRepository" => self.create_repository(req),
274            "DescribeRepository" => self.describe_repository(req),
275            "UpdateRepository" => self.update_repository(req),
276            "DeleteRepository" => self.delete_repository(req),
277            "ListRepositories" => self.list_repositories(req),
278            "ListRepositoriesInDomain" => self.list_repositories_in_domain(req),
279            "GetRepositoryEndpoint" => self.get_repository_endpoint(req),
280            "AssociateExternalConnection" => self.associate_external_connection(req),
281            "DisassociateExternalConnection" => self.disassociate_external_connection(req),
282            "GetRepositoryPermissionsPolicy" => self.get_repository_policy(req),
283            "PutRepositoryPermissionsPolicy" => self.put_repository_policy(req),
284            "DeleteRepositoryPermissionsPolicy" => self.delete_repository_policy(req),
285            // packages
286            "ListPackages" => self.list_packages(req),
287            "DescribePackage" => self.describe_package(req),
288            "DeletePackage" => self.delete_package(req),
289            "PutPackageOriginConfiguration" => self.put_package_origin(req),
290            // package versions
291            "ListPackageVersions" => self.list_package_versions(req),
292            "DescribePackageVersion" => self.describe_package_version(req),
293            "DeletePackageVersions" => self.delete_package_versions(req),
294            "DisposePackageVersions" => self.dispose_package_versions(req),
295            "UpdatePackageVersionsStatus" => self.update_package_versions_status(req),
296            "CopyPackageVersions" => self.copy_package_versions(req),
297            "PublishPackageVersion" => self.publish_package_version(req),
298            "GetPackageVersionReadme" => self.get_package_version_readme(req),
299            "GetPackageVersionAsset" => self.get_package_version_asset(req),
300            "ListPackageVersionAssets" => self.list_package_version_assets(req),
301            "ListPackageVersionDependencies" => self.list_package_version_dependencies(req),
302            // package groups
303            "CreatePackageGroup" => self.create_package_group(req),
304            "DescribePackageGroup" => self.describe_package_group(req),
305            "UpdatePackageGroup" => self.update_package_group(req),
306            "DeletePackageGroup" => self.delete_package_group(req),
307            "ListPackageGroups" => self.list_package_groups(req),
308            "ListSubPackageGroups" => self.list_sub_package_groups(req),
309            "ListAllowedRepositoriesForGroup" => self.list_allowed_repositories_for_group(req),
310            "GetAssociatedPackageGroup" => self.get_associated_package_group(req),
311            "ListAssociatedPackages" => self.list_associated_packages(req),
312            "UpdatePackageGroupOriginConfiguration" => self.update_package_group_origin(req),
313            // auth + tags
314            "GetAuthorizationToken" => self.get_authorization_token(req),
315            "TagResource" => self.tag_resource(req),
316            "UntagResource" => self.untag_resource(req),
317            "ListTagsForResource" => self.list_tags_for_resource(req),
318            _ => Err(AwsServiceError::action_not_implemented(
319                self.service_name(),
320                action,
321            )),
322        }
323    }
324}
325
326include!("handlers.rs");